ArticleProcessor - 文章处理器
ArticleProcessor 是 ArtiPub 的核心处理引擎,提供文章预处理功能,包括图片压缩、图片上传和自定义中间件处理。它通过管道架构处理 Markdown 文件,允许开发者通过中间件扩展功能。
概述
ArticleProcessor 类负责在发布前转换 Markdown 内容。它会自动:
- 压缩图片以减小文件大小
- 上传图片到云存储(GitHub 或自定义)
- 注入唯一文章 ID 用于跟踪
- 通过自定义中间件处理内容
构造函数
ts
class ArticleProcessor {
constructor(option: ArticleProcessorOption);
}
ArticleProcessorOption
ts
interface ArticleProcessorOption {
/**
* 图片压缩设置
* @default { quality: 80, compressed: true }
*/
compressedOptions?: {
/**
* 是否压缩图片
* @default true
*/
compressed?: boolean;
/**
* 压缩质量 (1-100)
* @default 80
*/
quality?: number;
};
/**
* 图片上传配置
* 可以是 GitHub 配置或自定义上传函数
*/
uploadImgOption: UploadImgOption;
}
UploadImgOption 类型
ts
/**
* GitHub 图床配置
*/
interface GithubPicBedOption {
owner: string; // GitHub 用户名或组织名
repo: string; // 仓库名称
dir: string; // 仓库中的目录路径
branch: string; // 目标分支(通常是 'main' 或 'master')
token: string; // GitHub Personal Access Token
commit_author: string; // 提交作者名称
commit_email: string; // 提交作者邮箱
}
/**
* 自定义上传函数
* @param imgFilePath - 本地图片文件路径
* @returns Promise 返回上传后的图片 URL
*/
type UploadImg = (imgFilePath: string) => Promise<string>;
/**
* 上传选项可以是 GitHub 配置或自定义函数
*/
type UploadImgOption = GithubPicBedOption | UploadImg;
方法
processMarkdown
处理 Markdown 文件并通过配置的管道。
ts
processMarkdown(filePath: string): Promise<ArticleProcessResult>
参数:
filePath
(string): Markdown 文件的绝对路径
返回值:
Promise<ArticleProcessResult>
: 处理后的内容
ts
interface ArticleProcessResult {
content: string; // 处理后的 Markdown 内容
}
use
添加自定义中间件到处理管道。
ts
use(middleware: Middleware): ArticleProcessor
参数:
middleware
(Middleware): 自定义中间件函数
返回值:
ArticleProcessor
: 返回自身以支持链式调用
中间件
中间件允许您通过操作 Markdown AST(抽象语法树)来自定义处理管道。
中间件类型定义
ts
type Middleware = (context: ProcessorContext, visitor: TVisitor, next: Next) => Promise<void>;
interface ProcessorContext {
option: ArticleProcessorOption;
filePath: string;
}
type Next = () => void;
type TVisitor = (
testOrVisitor: Visitor | Test,
visitorOrReverse: Visitor | boolean | null | undefined,
maybeReverse?: boolean | null | undefined
) => void;
编写自定义中间件
ts
const customMiddleware: Middleware = async (context, visit, next) => {
// 访问 AST 中的特定节点类型
visit("heading", (node, index, parent) => {
// 修改标题节点
if (node.depth === 1) {
// 处理 h1 标题
console.log("找到标题:", node.children[0].value);
}
});
// 访问图片节点
visit("image", (node) => {
// 处理图片 URL
console.log("找到图片:", node.url);
});
// 重要:始终调用 next() 以继续管道
next();
};
完整示例
基础使用 - GitHub 图片托管
ts
import { ArticleProcessor } from "@artipub/core";
import path from "path";
// 使用环境变量配置
const processor = new ArticleProcessor({
compressedOptions: {
quality: 85,
compressed: true,
},
uploadImgOption: {
owner: process.env.GITHUB_OWNER!,
repo: process.env.GITHUB_REPO!,
dir: "blog/images",
branch: "main",
token: process.env.GITHUB_TOKEN!,
commit_author: "ArtiPub Bot",
commit_email: "bot@example.com",
},
});
// 处理 Markdown 文件
const result = await processor.processMarkdown(path.resolve(__dirname, "./articles/my-post.md"));
console.log("处理后的内容:", result.content);
自定义图片上传函数
ts
import { ArticleProcessor } from "@artipub/core";
import { uploadToS3 } from "./utils/s3-upload";
const processor = new ArticleProcessor({
uploadImgOption: async (imgFilePath: string) => {
// 自定义上传逻辑(如:AWS S3、Cloudinary 等)
const url = await uploadToS3(imgFilePath);
return url;
},
});
高级用法:使用多个中间件
ts
import { ArticleProcessor } from "@artipub/core";
import { Heading, Text, Image } from "mdast";
const processor = new ArticleProcessor({
uploadImgOption: {
/* ... */
},
});
// 中间件 1:添加阅读时间估算
processor.use(async (context, visit, next) => {
let wordCount = 0;
visit("text", (node: Text) => {
wordCount += node.value.split(/\s+/).length;
});
const readingTime = Math.ceil(wordCount / 200); // 每分钟 200 字
// 在文档开头添加阅读时间
visit("root", (node) => {
const readingTimeNode = {
type: "paragraph",
children: [
{
type: "text",
value: `⏱️ 阅读时间:${readingTime} 分钟`,
},
],
};
node.children.unshift(readingTimeNode);
});
next();
});
// 中间件 2:为图片添加水印
processor.use(async (context, visit, next) => {
visit("image", (node: Image) => {
// 为图片 URL 添加水印参数
if (!node.url.includes("?")) {
node.url += "?watermark=artipub";
}
});
next();
});
// 中间件 3:将相对链接转换为绝对链接
processor.use(async (context, visit, next) => {
const baseUrl = "https://myblog.com";
visit("link", (node) => {
if (node.url.startsWith("./") || node.url.startsWith("../")) {
node.url = new URL(node.url, baseUrl).href;
}
});
next();
});
节点类型参考
中间件中可以访问的常见节点类型:
节点类型 | 描述 | 属性 |
---|---|---|
root | 文档根节点 | children[] |
heading | 标题 (h1-h6) | depth , children[] |
paragraph | 段落 | children[] |
text | 文本内容 | value |
image | 图片 | url , alt , title |
link | 链接 | url , children[] |
code | 代码块 | lang , value |
inlineCode | 内联代码 | value |
blockquote | 引用 | children[] |
list | 列表 | ordered , children[] |
listItem | 列表项 | children[] |
table | 表格 | children[] |
emphasis | 斜体文本 | children[] |
strong | 粗体文本 | children[] |
最佳实践
始终调用
next()
:在中间件中忘记调用next()
会停止管道。顺序很重要:中间件按添加顺序执行。图片处理中间件应在发布之前执行。
错误处理:在 try-catch 块中包装异步操作:
tsprocessor.use(async (context, visit, next) => { try { // 您的异步操作 await someAsyncOperation(); } catch (error) { console.error("中间件错误:", error); // 决定是继续还是抛出异常 } next(); });
性能:避免在中间件中进行繁重操作。在可能的情况下考虑缓存结果。
测试:在添加到处理器之前独立测试中间件:
ts// 单独测试中间件 const testMiddleware = async () => { const mockContext = { /* ... */ }; const mockVisit = jest.fn(); const mockNext = jest.fn(); await myMiddleware(mockContext, mockVisit, mockNext); expect(mockNext).toHaveBeenCalled(); };
API 参考
有关访问者模式和 AST 操作的更多详细信息:
- unist-util-visit - 访问者工具文档
- mdast - Markdown AST 规范
- unified - 内容处理生态系统