Mintlify ↗ 作为下一代技术文档平台,以其现代化的 MDX 支持和简洁易用的界面,深受技术写作者的喜爱。
我在写作的过程中面临的一个关键痛点:需要频繁在编辑器和浏览器之间切换来预览文档效果。这些琐碎的操作严重影响了写作流畅度。
为了解决这个问题,我在 FlashMintlify 插件中实现了一个强大的功能:VSCode 内预览 Mintlify 项目站点。这个功能让技术写作真正回归本质——专注于思考如何进行内容架构,而不是被技术细节打断写作思路。
演示和下载#
- 功能演示:
- 插件功能完全演示:一行代码不写完全用 AI 实现 VSCode 插件:FlashMintlify ↗
- VSCode 插件市场下载:FlashMintlify ↗
- GitHub 仓库:https://github.com/Match-Yang/FlashMintlify ↗
功能点详解#
用户痛点分析#
在使用 Mintlify 进行技术写作时,开发者通常需要:
- 在终端运行
mint dev
启动本地开发服务器 - 在浏览器中打开
localhost:3000
查看效果 - 每次修改文档后切换到浏览器查看变化
这种工作流程不仅效率低下,还容易打断写作思路。
功能设计思路#
FlashMintlify 的预览功能设计围绕以下核心原则:
- 一键预览:通过编辑器标题栏的预览按钮,一键打开当前文档的预览
- 多种预览模式:支持编辑器内预览、全屏预览和浏览器预览三种模式
- 智能路径解析:自动将 MDX 文件路径转换为 Mintlify 的路由路径
- 实时同步:文档内容变化时自动刷新预览
- 可视化配置:提供友好的配置界面管理预览选项
架构图解#
整体架构流程#
graph TB
A[用户打开 MDX 文档] --> B[点击编辑器标题栏预览按钮]
B --> C[执行 flashMintlify.preview.open 命令]
C --> D[获取当前文档路径]
D --> E[构建内部路径]
E --> F[读取预览配置]
F --> G{预览模式?}
G -->|beside| H[PreviewPanel.createOrShow
ViewColumn.Beside] G -->|fullscreen| I[PreviewPanel.createOrShow
ViewColumn.Active] G -->|browser| J[vscode.env.openExternal
在系统浏览器打开] H --> K[创建 WebviewPanel] I --> K K --> L[设置 iframe 指向 mint dev 服务器] L --> M[在 VSCode 内显示预览] N[用户点击预览设置] --> O[打开 PreviewSettingsPanel] O --> P[可视化编辑端口和模式] P --> Q[保存到 workspace 配置] Q --> R[实时更新预览面板]
ViewColumn.Beside] G -->|fullscreen| I[PreviewPanel.createOrShow
ViewColumn.Active] G -->|browser| J[vscode.env.openExternal
在系统浏览器打开] H --> K[创建 WebviewPanel] I --> K K --> L[设置 iframe 指向 mint dev 服务器] L --> M[在 VSCode 内显示预览] N[用户点击预览设置] --> O[打开 PreviewSettingsPanel] O --> P[可视化编辑端口和模式] P --> Q[保存到 workspace 配置] Q --> R[实时更新预览面板]
时序交互图#
sequenceDiagram
participant U as 用户
participant E as VSCode编辑器
participant CMD as 命令处理器
participant PP as PreviewPanel
participant WV as WebviewPanel
participant MS as Mint Dev服务器
U->>E: 在 MDX 文件中点击预览按钮
E->>CMD: 触发 flashMintlify.preview.open
CMD->>CMD: 获取当前文档路径和配置
CMD->>CMD: 构建目标 URL
alt 预览模式: beside/fullscreen
CMD->>PP: createOrShow(url, viewColumn)
PP->>WV: 创建 webview 面板
WV->>MS: 通过 iframe 加载页面
MS-->>WV: 返回渲染内容
WV-->>E: 在编辑器中显示预览
else 预览模式: browser
CMD->>E: vscode.env.openExternal(url)
E->>MS: 在系统浏览器打开
end
Note over U,MS: 当文档内容发生变化时
E->>CMD: 文档保存事件
CMD->>PP: 自动刷新预览内容
PP->>WV: 更新 iframe src
类设计架构#
classDiagram
class PreviewPanel {
+static currentPanel PreviewPanel
-_panel vscode.WebviewPanel
-_extensionUri vscode.Uri
+static createOrShow(extensionUri, url, viewColumn)
+update(url string)
+dispose()
}
class PreviewSettingsPanel {
+static currentPanel PreviewSettingsPanel
+static createOrShow(extensionUri)
+handleSave(data SettingsData)
+getFields() SettingsField[]
+getTitle() string
+getWebviewScript() string
}
class SettingsPanel {
#_panel vscode.WebviewPanel
#_extensionUri vscode.Uri
+updateContent(fields SettingsField[])
+dispose()
#getBaseWebviewScript() string
}
class Extension {
+activate(context)
+registerPreviewCommands()
+checkDocsJsonExists() boolean
}
PreviewSettingsPanel --|> SettingsPanel
Extension --> PreviewPanel
Extension --> PreviewSettingsPanel
PreviewPanel --> WebviewPanel
PreviewSettingsPanel --> Configuration
代码实现#
核心预览命令实现#
const openPreviewCommand = vscode.commands.registerCommand('flashMintlify.preview.open', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active editor found');
return;
}
const doc = editor.document;
const filePath = doc.uri.fsPath;
// 构建内部路径
const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!root) {
vscode.window.showErrorMessage('No workspace folder found');
return;
}
// 将文件路径转换为 Mintlify 路由路径
const rel = path.relative(root, filePath)
.replace(/\\/g, '/')
.replace(/\.(md|mdx)$/i, '');
// 特殊处理:根目录的 index.mdx 应该渲染为 /,而不是 /index
const internalPath = rel === 'index' ? '/' : '/' + rel;
// 读取预览配置
const cfg = vscode.workspace.getConfiguration('flashMintlify');
const configuredPort = cfg.get<number>('preview.port', 3000);
const mode = cfg.get<'beside' | 'fullscreen' | 'browser'>('preview.mode', 'browser');
const targetUrl = `http://localhost:${configuredPort}${internalPath}`;
// 根据模式选择预览方式
if (mode === 'beside') {
PreviewPanel.createOrShow(context.extensionUri, targetUrl, vscode.ViewColumn.Beside);
} else if (mode === 'fullscreen') {
PreviewPanel.createOrShow(context.extensionUri, targetUrl, vscode.ViewColumn.Active);
} else {
vscode.env.openExternal(vscode.Uri.parse(targetUrl));
}
});
typescriptPreviewPanel 核心实现#
export class PreviewPanel {
public static currentPanel: PreviewPanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private readonly _extensionUri: vscode.Uri;
public static createOrShow(
extensionUri: vscode.Uri,
url: string,
viewColumn: vscode.ViewColumn = vscode.ViewColumn.Two
) {
if (PreviewPanel.currentPanel) {
// 如果已经有预览面板,只更新内容,避免位置变化
PreviewPanel.currentPanel.update(url);
return;
}
const panel = new PreviewPanel(extensionUri, url, viewColumn);
PreviewPanel.currentPanel = panel;
}
private constructor(extensionUri: vscode.Uri, url: string, viewColumn: vscode.ViewColumn) {
this._extensionUri = extensionUri;
// 根据 viewColumn 决定标题和图标
const isFullscreen = viewColumn === vscode.ViewColumn.Active;
const title = isFullscreen ? 'Mintlify Preview (Fullscreen)' : 'Mintlify Preview';
this._panel = vscode.window.createWebviewPanel(
'flashMintlifyPreview',
title,
{ viewColumn, preserveFocus: !isFullscreen },
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(extensionUri, 'media'),
vscode.Uri.joinPath(extensionUri, 'out', 'webview')
]
}
);
this._panel.onDidDispose(() => this.dispose());
this.update(url);
}
public update(url: string) {
const nonce = String(Date.now());
const csp = `default-src 'none'; img-src vscode-resource: https: data:; script-src 'nonce-${nonce}'; style-src 'unsafe-inline'; frame-src ${url};`;
// 使用 iframe 嵌入 Mintlify 预览页面
this._panel.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="${csp}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mintlify Preview</title>
<style>
html, body, iframe { height: 100%; width: 100%; padding: 0; margin: 0; }
body { background: var(--vscode-editor-background); }
iframe { border: 0; }
</style>
</head>
<body>
<iframe src="${url}" allow="clipboard-read; clipboard-write"></iframe>
</body>
</html>`;
}
}
typescript预览设置面板实现#
export class PreviewSettingsPanel extends SettingsPanel {
protected async handleSave(data: SettingsData): Promise<void> {
const cfg = vscode.workspace.getConfiguration('flashMintlify');
const portValue = Number(data['preview.port']?.value ?? 3000);
const modeValue = (data['preview.mode']?.value ?? 'browser') as 'beside' | 'fullscreen' | 'browser';
// 验证端口值
if (isNaN(portValue) || portValue <= 0 || portValue > 65535) {
vscode.window.showErrorMessage(`Invalid port number: ${portValue}`);
return;
}
try {
// 保存配置到工作区
await cfg.update('preview.port', portValue, vscode.ConfigurationTarget.Workspace);
await cfg.update('preview.mode', modeValue, vscode.ConfigurationTarget.Workspace);
const action = await vscode.window.showInformationMessage(
'Preview options saved successfully!',
'OK',
'Reload Window'
);
if (action === 'Reload Window') {
vscode.commands.executeCommand('workbench.action.reloadWindow');
}
} catch (error) {
vscode.window.showErrorMessage('Failed to save preview options: ' + error);
}
}
protected getFields(): SettingsField[] {
const cfg = vscode.workspace.getConfiguration('flashMintlify');
const port = cfg.get<number>('preview.port', 3000);
const mode = cfg.get<string>('preview.mode', 'browser');
return [
{
name: 'preview.port',
type: 'number',
label: 'Preview port',
description: "Port where 'mint dev' is running",
defaultValue: String(port),
currentValue: String(port),
placeholder: '3000'
},
{
name: 'preview.mode',
type: 'select',
label: 'Preview mode',
description: 'Choose how to open the preview',
options: ['beside', 'fullscreen', 'browser'],
defaultValue: mode,
currentValue: mode
}
];
}
}
typescript实现要点和注意事项#
- 路径转换逻辑:需要正确处理 Windows 和 Unix 路径分隔符,并将
.md
和.mdx
扩展名移除 - 特殊路径处理:根目录的
index.mdx
文件应该映射到/
路径,而不是/index
- 面板管理:使用单例模式确保同一时间只有一个预览面板,避免资源浪费
- 安全策略:正确配置 Content Security Policy,允许 iframe 加载本地开发服务器内容
- 错误处理:对无效端口号、文件路径等进行适当的验证和错误提示
简单总结#
VSCode 内预览功能极大地提升了使用 Mintlify 进行技术写作的效率和体验:
- 无缝集成:在编辑器内直接预览,无需切换窗口
- 多种模式:支持分屏、全屏和浏览器三种预览方式,满足不同场景需求
- 智能路径:自动处理文件路径到 URL 路径的转换
- 配置友好:可视化的设置界面,轻松配置端口和预览模式
- 实时同步:文档变化时自动更新预览内容
这个功能让技术写作者能够真正专注于内容创作,而不是被技术细节分散注意力。它体现了 FlashMintlify 的核心理念:让写作回归本质,把重心放在内容架构和核心价值传达上。
后续改进方向#
- 智能端口检测:自动检测
mint dev
运行的端口 - 预览状态同步:支持预览窗口和编辑器光标位置同步
- 快捷键支持:添加快捷键快速切换预览模式
- 错误页面优化:当服务器未启动时显示友好的提示页面
体验 FlashMintlify,让技术写作变得更加高效和愉悦!