<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>liangerwen's☻blog</title>
        <link>https://blog-nine-navy-52.vercel.app</link>
        <description>这瓜娃子懒得很，什么都没有留哈！</description>
        <lastBuildDate>Mon, 20 Apr 2026 09:22:17 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>zh-CN</language>
        <copyright>All rights reserved 2026, liangerwen</copyright>
        <item>
            <title><![CDATA[React-Customer: 一个用于React项目定制化的解耦的库]]></title>
            <link>https://blog-nine-navy-52.vercel.app/posts/React-Customer: 一个用于React项目定制化的解耦的库</link>
            <guid>React-Customer: 一个用于React项目定制化的解耦的库</guid>
            <pubDate>Thu, 15 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[简介React-Customer 是一个专为 React 技术栈设计的定制化库，它的核心目标是实现平台逻辑与定制逻辑的解耦，从而提高项目的可维护性、可扩展性和可复用性。在 SaaS（Software as a Service）平台开发中，经常需要为不同客户提供定制化的功能，而传统的开发方式往往导致代码耦合度高、难以维护。React-Customer 通过提供一套完整的定制化解决方案，使平台开发者和定制开发者能够独立工作，同时保持良好的交互。核心架构React-Customer 采用了基于 React Context 的提供者-消费者模式，实现了平台组件与定制插件之间的无缝集成。
关键组件CustomProvider：作为定制系统的入口点，维护 UI 元素、平台 API、定制 API 和定制组件的中央注册表。withCustom HOC：包装平台组件，使其可被定制，通过注册带有 data-id 属性的元素、暴露平台 API 给插件以及消费插件的定制 API。withDefineCustom HOC：创建可以修改平台组件的插件组件，通过访问平台 API、提供定制 API 以及使用元素操作工具来操作 UI 元素。元素操作系统：提供一系列工具函数，用于修改组件树中的 React 元素，这些工具通过merge函数提供给插件。工作原理React-Customer 的工作原理基于以下几个关键概念：组件标识与定制平台组件使用 data-id 属性来标识可定制的元素，这使得插件可以精确地定位和修改特定元素，而无需直接耦合到其内部结构。双向 API 通信该库使用双向 API 系统，实现平台组件和插件之间的通信：
平台 API：从平台组件向插件暴露功能，通过平台组件中的 exposeApi 实现。
定制 API：从插件向平台组件暴露功能，通过插件中的 useImperativeHandle 实现。
这种双向 API 系统允许平台和定制逻辑之间松散耦合，实现独立开发和维护。元素操作机制插件通过merge函数提供的工具来修改平台组件的元素，包括：appendBefore：在目标元素之前添加新元素appendAfter：在目标元素之后添加新元素replace：完全替换目标元素replaceChildren：替换目标元素的子元素replaceProps：修改目标元素的属性remove：移除目标元素insertBefore：在目标元素之前插入子元素insertAfter：在目标元素之后插入子元素数据流与组件交互React-Customer 通过中心化的上下文系统实现平台组件和插件之间的双向通信：
使用示例安装pnpm install react-customer -S设置 CustomProvider首先，在应用的入口点设置 CustomProvider 并传入插件数组：import { CustomProvider } from "react-customer";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import plugins from "./plugins.tsx";

// 这里插件可根据不同客户应用不同定制化插件
createRoot(document.getElementById("root")!).render(
  <CustomProvider plugins={plugins}>
    <App />
  </CustomProvider>
);创建平台组件使用 withCustom HOC 包装平台组件，使其可定制：import { withCustom } from "react-customer";
import { Button } from "antd";

export interface AppExposeApi {
  clickButton: () => void;
}

export interface AppCustomApi {
  setInputText: (text: string) => void;
}

const App = withCustom<AppCustomApi, AppExposeApi>(
  "App",
  ({ customApi, exposeApi, wrap }) => {
    const clickButton = () => {
      customApi?.setInputText?.("点击了Button1");
    };

    exposeApi({ clickButton });

    return wrap(
      <>
        <Button type="primary" onClick={clickButton} data-id="button-01">
          我是Button1
        </Button>
        <Button
          data-id="button-02"
          type="dashed"
          onClick={() => {
            customApi?.setInputText?.("点击了Button2");
          }}
        >
          我是Button2
        </Button>
      </>
    );
  }
);

export default App;创建定制插件使用 withDefineCustom HOC 创建定制插件：import { forwardRef, useState, useImperativeHandle } from "react";
import { withDefineCustom } from "react-customer";
import { Button, ButtonProps, Input } from "antd";

const AppPlugin = withDefineCustom<{
  clickButton: () => void;
}>(
  "App",
  forwardRef(({ merge, platformApi }, ref) => {
    const [text, setText] = useState("");

    const setInputText = (txt: string) => setText(txt);

    useImperativeHandle(ref, () => {
      return { setInputText };
    });

    return merge((element) => {
      element.replaceChildren("button-02", "我是Button2【定制按钮-A】");
      element.replaceProps<ButtonProps>("button-02", {
        onClick: () => {
          setText("点击了定制按钮Button2【A】");
        },
      });
      element.appendBefore(
        "button-01",
        <Button
          onClick={() => {
            platformApi?.clickButton?.();
          }}
        >
          我是定制按钮Button3【A】
        </Button>
      );
      element.appendAfter(
        "button-02",
        <Input value={text} onChange={(e) => setText(e.target.value)} />
      );
    });
  })
);

export default [AppPlugin];应用场景React-Customer 特别适用于以下场景：SaaS 平台定制化
对于需要为不同客户提供定制化功能的 SaaS 平台，React-Customer 提供了一种优雅的方式来实现定制，而不需要为每个客户维护单独的代码分支。大型企业应用
在大型企业应用中，不同部门或业务线可能需要对共享组件进行定制。React-Customer 允许基于同一套核心组件，为不同业务需求提供定制化解决方案。白标产品开发
对于需要以不同品牌或外观提供的白标产品，React-Customer 可以轻松实现 UI 的定制化，包括样式、布局和功能的调整。插件化架构
对于希望实现插件化架构的应用，React-Customer 提供了一种结构化的方式来定义和集成插件，使应用更具扩展性。优势与特点解耦平台与定制逻辑：平台开发者和定制开发者可以独立工作，减少协作成本。非侵入式定制：通过 data-id 属性标识元素，实现对组件的精确定制，而不需要修改原始组件代码。双向 API 通信：平台组件和定制插件之间可以双向通信，实现复杂的交互逻辑。类型安全：通过 TypeScript 接口定义平台 API 和定制 API，提供类型安全的开发体验。灵活的元素操作：提供丰富的元素操作工具，满足各种定制需求。总结React-Customer 是一个为 React 设计的定制化库，通过提供结构化的方式来解耦平台逻辑和定制逻辑，使得 SaaS 应用的定制化开发更加高效和可维护。
它的核心架构基于 React Context 和高阶组件，实现了平台组件和定制插件之间的无缝集成。
无论是需要为不同客户提供定制化功能的 SaaS 平台，还是需要实现插件化架构的大型企业应用，React-Customer 都提供了一种优雅而强大的解决方案。
附上 github 地址：https://github.com/liangerwen/react-customer]]></content:encoded>
            <enclosure url="https://tc.alcy.cc/i/2024/04/21/6624129ca3dfc.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[基于 Next.js + ContentLayer + MDX 构建无后端博客系统]]></title>
            <link>https://blog-nine-navy-52.vercel.app/posts/基于 Next.js + ContentLayer + MDX 构建无后端博客系统</link>
            <guid>基于 Next.js + ContentLayer + MDX 构建无后端博客系统</guid>
            <pubDate>Sat, 22 Mar 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[引言在构建个人博客或技术文档时，无后端方案因其简单、高效和低成本而备受青睐。本文将介绍如何使用 Next.js、ContentLayer 和 MDX 构建一个无后端博客系统（本博客当前采用的方案），并探讨如何后续迁移到 MDX-Bundler 和 GitHub API，利用 GitHub Issues 作为文章存储源。技术栈简介Next.js：React 框架，支持 SSR、SSG 和 API 路由，适合构建高性能博客。ContentLayer：将 Markdown 或 MDX 文件转换为类型安全的 JSON 数据，方便在 Next.js 中使用。MDX：支持在 Markdown 中嵌入 React 组件，增强文章的表现力。MDX-Bundler：将 MDX 文件编译为 React 组件，支持动态加载和自定义组件。GitHub API：通过 GitHub Issues 存储文章内容，实现无后端数据管理。使用 Next.js + ContentLayer + MDX 构建博客系统初始化项目npx create-next-app@latest my-blog
cd my-blog
npm install contentlayer @mdx-js/loader配置 ContentLayer在项目根目录创建 contentlayer.config.ts 文件：import { defineDocumentType, makeSource } from "contentlayer/source-files";

export const Post = defineDocumentType(() => ({
  name: "Post",
  filePathPattern: `**/*.mdx`,
  fields: {
    title: { type: "string", required: true },
    date: { type: "date", required: true },
  },
  computedFields: {
    slug: {
      type: "string",
      resolve: (post) => post._raw.flattenedPath,
    },
  },
}));

export default makeSource({
  contentDirPath: "data/posts",
  documentTypes: [Post],
});创建文章在 data/posts 目录下创建 Markdown 或 MDX 文件：---
title: "Hello, Next.js!"
date: 2023-10-01
---

This is a blog post written in **MDX**.实现文章列表页在 src/app/page.tsx 中实现文章列表：import { allPosts } from "contentlayer/generated";
import Link from "next/link";

export default function Home() {
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {allPosts.map((post) => (
          <li key={post.slug}>
            <Link href={`/posts/${post.slug}`}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}创建 mdx 渲染组件在 src/components/mdx.tsx 中实现 mdx 渲染组件：import { useMDXComponent } from "next-contentlayer/hooks";

export interface MdxProps {
  code: string;
}

export default function Mdx({ code }: MdxProps) {
  const Component = useMDXComponent(code);
  return <Component />;
}实现文章详情页在 src/app/posts/[slug].tsx 中实现文章详情页：import Mdx from "@/src/components/mdx";
import { allPosts } from "contentlayer/generated";

interface IProps {
  params: { slug: string[] };
}

export default function Post({ params }: IProps) {
  const slug = decodeURIComponent(params.slug.join("/"));
  const postIdx = allPosts.findIndex((p) => p.slug === slug);
  const post = allPosts[postIdx];
  if (!post) return notFound();

  return (
    <div>
      <h1>{post.title}</h1>
      <div>
        <Mdx code={post.body.code} />
      </div>
    </div>
  );
}迁移到 MDX-Bundler + GitHub API安装 MDX-Bundlernpm install mdx-bundler根据 repo 和 name 获取 issuesconst REPO = "your repo";
const NAME = "your name";

export const fetchGithubIssueList = (type: "page" | "post", current: number) =>
  fetch(
    `https://api.github.com/search/issues?q=repo:${REPO}+state:open+author:${NAME}+${encodeURIComponent(
      `[${type}]`
    )}+in:title&per_page=10&sort=updated&page=${current}`
  ).then((res) => res.json());

export const fetchGithubIssueDetail = (id: number) =>
  fetch(`https://api.github.com/repos/${REPO}/issues/${id}`).then((res) =>
    res.json()
  );使用 MDX-Bundler 编译文章import { bundleMDX } from "mdx-bundler";

export const parseMDX = (content: string) =>
  bundleMDX({
    source: content,
    esbuildOptions: (opts) => {
      opts.target = "es2020";
      return opts;
    },
  });改写文章列表页面import { fetchGithubIssueList } from "@/src/utils/github";
import Link from "next/link";

export default async function Home() {
  const allPosts = await fetchGithubIssueList("post", 1);
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {allPosts.items.map((post) => (
          <li key={post.id}>
            <Link href={`/posts/${post.number}`}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}改写文章详情页面import { parseMDX } from "@/src/utils/parse-mdx";
import { fetchGithubIssueDetail } from "@/src/utils/github";

interface IProps {
  params: { slug: string };
}

export default async function Post({ params }: IProps) {
  const post = await fetchGithubIssueDetail(params.slug);
  if (post.status === "404") return notFound();
  const { code } = await parseMDX(post.body);
  return (
    <div>
      <h1>{post.title}</h1>
      <div>
        <Mdx code={code} />
      </div>
    </div>
  );
}总结通过 Next.js + ContentLayer + MDX，我们可以快速构建一个无后端博客系统。而迁移到 MDX-Bundler + GitHub API 后，我们可以利用 GitHub Issues 作为文章存储源，实现更灵活的内容管理。这种方案不仅简单高效，还能充分利用现有的工具和平台，适合个人开发者和小型团队。]]></content:encoded>
            <enclosure url="https://tc.alcy.cc/i/2024/04/21/662417e645b56.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[手写题盘点]]></title>
            <link>https://blog-nine-navy-52.vercel.app/posts/手写题盘点</link>
            <guid>手写题盘点</guid>
            <pubDate>Tue, 25 Feb 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[广度遍历深克隆const getEmpty = (o) => {
  if (Array.isArray(o)) return [];
  if (o !== null && typeof o === "object") return {};
  return o;
};

const cloneBfs = (a) => {
  const root = getEmpty(a),
    queue = [{ origin: a, copy: root }],
    map = new Map();

  while (queue.length) {
    const { origin, copy } = queue.shift();
    for (let i in origin) {
      const empty = getEmpty(origin[i]);
      if (empty === origin[i]) {
        copy[i] = origin[i];
      } else {
        if (map.has(origin[i])) {
          copy[i] = map.get(origin[i]);
          continue;
        }
        copy[i] = empty;
        queue.push({ origin: origin[i], copy: copy[i] });
        map.set(origin[i], empty);
      }
    }
  }
  return root;
};限制 promise 并发数量const pLimit = (limit) => {
  let count = 0;
  const task = [];
  return async (fn) => {
    if (count >= limit) {
      await new Promise((r) => task.push(r));
    }
    count++;
    try {
      const ret = await fn();
      return ret;
    } finally {
      count--;
      if (task.length) {
        task.shift()();
      }
    }
  };
};]]></content:encoded>
            <enclosure url="https://tc.alcy.cc/i/2024/04/21/6624129dd5d20.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[分享一些前端好用的库和网站]]></title>
            <link>https://blog-nine-navy-52.vercel.app/posts/分享一些前端好用的库和网站</link>
            <guid>分享一些前端好用的库和网站</guid>
            <pubDate>Wed, 15 Jan 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[| 网址                                                         | 简介                                                                  |
| ------------------------------------------------------------ | --------------------------------------------------------------------- |
| React Bits                      | 最大且最具创意的动画 React 组件库                                     |
| react-spring                 | react-spring 是一个跨平台的 spring-physics first 动画库             |
| uiverse                                | 使用 CSS 或 Tailwind 制作的开源组件合集                               |
| Whirl                           | 轻松搞定 CSS 加载动画                                                 |
| css-loaders                       | CSS 加载动画合集                                                      |
| free-icons        | 免费开源 icon 合集                                                    |
| Lucide                                 | 美丽&一致的图标 React Icon 组件                                       |
| iconify                  | 集成所有流行的图标集的框架                                            |
| jotai                                   | 一个采用原子方法进行全局 React 状态管理库                             |
| Zustand                      | 一个小型、快速且可扩展的 React 状态管理库                             |
| shadcn/ui                           | shadcn/ui 是一组设计精美、可访问的组件和一个代码分发平台            |
| css-pattern                       | CSS 渐变制作背景图合集                                                |
| css-generators                 | 广泛的 CSS 生成器                                                     |
| qr-code                            | 一个基于 SVG 的无框架、无依赖项、可定制、可制作动画的自定义 html 元素 |
| 21st                                     | 一个 ai 生成的基于 shadcn/ui 的组件库&模版                            |
| Best Of Js                           | 查找与前端相关的最佳开源项目的地方                                    |
| Logo.surf                           | 通过文本生成图标的网站                                                |
| Coupon.io                      | 生成优惠券样式 CSS 代码                                               |
| Matsu-theme for shadcn/ui | 吉卜力风格 shadcn-ui 主题                                             |]]></content:encoded>
            <enclosure url="https://tc.alcy.cc/i/2024/04/21/6624164d58150.webp
" length="0" type="image/webp"/>
        </item>
    </channel>
</rss>