Fast Car

Back

Mintlify 作为下一代技术文档平台,以其现代化的 MDX 支持和简洁易用的界面,深受技术写作者的喜爱。

我在写作的过程中面临的一个关键痛点:需要频繁在编辑器和浏览器之间切换来预览文档效果。这些琐碎的操作严重影响了写作流畅度。

为了解决这个问题,我在 FlashMintlify 插件中实现了一个强大的功能:VSCode 内预览 Mintlify 项目站点。这个功能让技术写作真正回归本质——专注于思考如何进行内容架构,而不是被技术细节打断写作思路。

演示和下载#

功能点详解#

用户痛点分析#

在使用 Mintlify 进行技术写作时,开发者通常需要:

  1. 在终端运行 mint dev 启动本地开发服务器
  2. 在浏览器中打开 localhost:3000 查看效果
  3. 每次修改文档后切换到浏览器查看变化

这种工作流程不仅效率低下,还容易打断写作思路。

功能设计思路#

FlashMintlify 的预览功能设计围绕以下核心原则:

  1. 一键预览:通过编辑器标题栏的预览按钮,一键打开当前文档的预览
  2. 多种预览模式:支持编辑器内预览、全屏预览和浏览器预览三种模式
  3. 智能路径解析:自动将 MDX 文件路径转换为 Mintlify 的路由路径
  4. 实时同步:文档内容变化时自动刷新预览
  5. 可视化配置:提供友好的配置界面管理预览选项

架构图解#

整体架构流程#

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[实时更新预览面板]

时序交互图#

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));
  }
});
typescript

PreviewPanel 核心实现#

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

实现要点和注意事项#

  1. 路径转换逻辑:需要正确处理 Windows 和 Unix 路径分隔符,并将 .md.mdx 扩展名移除
  2. 特殊路径处理:根目录的 index.mdx 文件应该映射到 / 路径,而不是 /index
  3. 面板管理:使用单例模式确保同一时间只有一个预览面板,避免资源浪费
  4. 安全策略:正确配置 Content Security Policy,允许 iframe 加载本地开发服务器内容
  5. 错误处理:对无效端口号、文件路径等进行适当的验证和错误提示

简单总结#

VSCode 内预览功能极大地提升了使用 Mintlify 进行技术写作的效率和体验:

  1. 无缝集成:在编辑器内直接预览,无需切换窗口
  2. 多种模式:支持分屏、全屏和浏览器三种预览方式,满足不同场景需求
  3. 智能路径:自动处理文件路径到 URL 路径的转换
  4. 配置友好:可视化的设置界面,轻松配置端口和预览模式
  5. 实时同步:文档变化时自动更新预览内容

这个功能让技术写作者能够真正专注于内容创作,而不是被技术细节分散注意力。它体现了 FlashMintlify 的核心理念:让写作回归本质,把重心放在内容架构和核心价值传达上。

后续改进方向#

  1. 智能端口检测:自动检测 mint dev 运行的端口
  2. 预览状态同步:支持预览窗口和编辑器光标位置同步
  3. 快捷键支持:添加快捷键快速切换预览模式
  4. 错误页面优化:当服务器未启动时显示友好的提示页面

体验 FlashMintlify,让技术写作变得更加高效和愉悦!

(Part1)用 AI IDE 实现 VSCode 插件系列:VSCode 内无缝预览 Mintlify 项目站点
https://astro-pure.js.org/blog/vscode-extension/p1-vscode-preview-mintlify-site
Author Oliver Yeung
Published at 2025年9月5日
Comment seems to stuck. Try to refresh?✨