Markdown编辑框架

参考文档: https://blog.csdn.net/gitblog_00200/article/details/151257287

milkdown——这款基于ProseMirror和Remark构建的插件驱动型所见即所得(WYSIWYG)Markdown编辑框架,正以其革命性的插件系统彻底改变这一现状。本文将深入剖析milkdown的核心架构、插件生态与实战应用,带你掌握如何构建高度定制化的编辑体验。

通过本文将获得:

  • 理解milkdown的插件驱动架构设计理念
  • 掌握核心模块的协作机制与扩展点
  • 学会从零构建自定义插件与编辑器配置
  • 实现高级功能如协作编辑、自定义快捷键和主题定制
  • 了解企业级应用的最佳实践与性能优化策略

架构总览:插件驱动的设计哲学

milkdown采用分层架构设计,通过上下文(Context)系统实现模块解耦,其核心可概括为"三横三纵"结构:

核心架构图

image.webp

核心模块协作流程

milkdown的初始化遵循严格的生命周期管理,各模块通过上下文系统有序交互:

image.webp

核心技术解析:从Context到插件生态

Context系统:应用状态的神经中枢

Context(上下文)系统是milkdown的核心,采用依赖注入模式管理应用状态。每个模块通过唯一键(Key)注册和访问上下文,实现松耦合通信:

// 定义上下文键
import { createCtx, Ctx } from '@milkdown/ctx';
 
const countCtx = createCtx(0);
 
// 在插件中使用上下文
function counterPlugin(ctx: Ctx) {
  // 获取当前值
  const count = ctx.get(countCtx);
  // 更新值
  ctx.set(countCtx, count + 1);
  
  // 监听变化
  return () => {
    const unsubscribe = ctx.onUpdate(countCtx, (newCount) => {
      console.log('Count updated:', newCount);
    });
  
    return unsubscribe;
  };
}

核心上下文键分类:

类别 关键上下文 作用
编辑器状态 editorStateCtx 管理ProseMirror状态
文档模型 schemaCtx 存储节点和标记定义
命令系统 commandsCtx 注册和执行命令
视图配置 editorViewOptionsCtx 自定义编辑器视图
插件配置 pluginConfigCtx 存储插件特定配置

插件架构:功能扩展的原子单元

milkdown插件采用"工厂函数+类"混合模式,每个插件包含注册逻辑和生命周期钩子:

import { Plugin, Editor } from '@milkdown/core';
import { SlashProvider } from '@milkdown/plugin-slash';
 
// 创建插件工厂
const mySlashPlugin = () => {
  return Plugin.create('my-slash-plugin', (ctx) => {
    // 初始化阶段
    return {
      // 插件加载时执行
      load: () => {
        ctx.set(slashConfigCtx, {
          trigger: '/',
          items: [/* 自定义命令项 */]
        });
      },
      // 编辑器视图创建后执行
      view: (editorView) => {
        const provider = new SlashProvider(editorView);
        return {
          update: () => provider.update(),
          destroy: () => provider.destroy()
        };
      }
    };
  });
};
 
// 使用插件
Editor.make()
  .use(mySlashPlugin())
  .create();

插件生命周期:

image.webp

文档模型:Markdown的结构化表示

milkdown使用Schema定义文档结构,包含节点(Nodes)和标记(Marks)两类元素:

// 定义自定义节点
import { NodeSchema } from '@milkdown/core';
 
const customNodeSchema: NodeSchema = {
  name: 'callout',
  content: 'block+',
  group: 'block',
  defining: true,
  attrs: {
    type: { default: 'note' },
    title: { default: '' }
  },
  parseDOM: [{
    tag: 'div.callout',
    getAttrs: (dom) => ({
      type: dom.dataset.type || 'note',
      title: dom.getAttribute('title') || ''
    })
  }],
  toDOM: (node) => [
    'div',
    { class: `callout callout-${node.attrs.type}`, title: node.attrs.title },
    0
  ]
};

核心节点类型:

节点类型 作用 示例
paragraph 段落文本 普通文本块
heading 标题 # 标题文本
code_block 代码块 js ...
list_item 列表项 - 列表内容
table 表格 表头,内容

实战指南:从零构建定制编辑器

快速开始:基础编辑器搭建

<!DOCTYPE html>
<html>
<head>
  <!-- 使用国内CDN -->
  <script src="https://cdn.tailwindcss.com"></script>
  <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
  <div id="editor"></div>
 
  <script type="module">
    import { Editor } from 'https://cdn.jsdelivr.net/npm/@milkdown/core@7/+esm';
    import { commonmark } from 'https://cdn.jsdelivr.net/npm/@milkdown/preset-commonmark@7/+esm';
    import { crepe } from 'https://cdn.jsdelivr.net/npm/@milkdown/crepe@7/+esm';
 
    Editor.make()
      .use(commonmark)    // 基础Markdown支持
      .use(crepe)         // 基础UI主题
      .select('#editor')  // 绑定DOM元素
      .create();          // 创建编辑器
  </script>
</body>
</html>

插件组合:构建全功能编辑器

import { Editor } from '@milkdown/core';
import { commonmark } from '@milkdown/preset-commonmark';
import { gfm } from '@milkdown/preset-gfm';
import { nord } from '@milkdown/theme-nord';
import { slash } from '@milkdown/plugin-slash';
import { history } from '@milkdown/plugin-history';
import { table } from '@milkdown/plugin-table';
import { tooltip } from '@milkdown/plugin-tooltip';
import { clipboard } from '@milkdown/plugin-clipboard';
 
// 全功能编辑器配置
const editor = Editor.make()
  .use(nord)                // Nord主题
  .use(commonmark)          // 基础Markdown
  .use(gfm)                 // GitHub风格扩展
  .use(history)             // 撤销/重做
  .use(clipboard)           // 增强剪贴板
  .use(table)               // 表格支持
  .use(tooltip)             // 悬浮提示
  .use(slash.configure((ctx) => {
    // 自定义slash命令
    ctx.set(slash.config, {
      items: [
        {
          id: 'custom-command',
          title: '插入自定义块',
          description: '添加带图标和样式的自定义内容块',
          command: (editor) => {
            editor
              .chain()
              .insertNode('callout', { type: 'tip', title: '提示' })
              .run();
          }
        }
      ]
    });
  }))
  .select('#app')
  .create();

自定义节点:扩展Markdown语法

import { Node, Mark, createNode } from '@milkdown/core';
import { InputRule } from '@milkdown/prose';
 
// 定义警告框节点
const calloutNode = createNode((utils) => {
  const id = 'callout';
  const attrs = {
    type: 'note',
    title: ''
  };
  
  return {
    id,
    attrs,
    schema: {
      attrs: {
        type: { default: attrs.type },
        title: { default: attrs.title }
      },
      content: 'block+',
      group: 'block',
      defining: true,
      parseDOM: [{
        tag: `div[data-type="${id}"]`,
        getAttrs: (dom) => ({
          type: dom.dataset.type || attrs.type,
          title: dom.getAttribute('title') || attrs.title
        })
      }],
      toDOM: (node) => [
        'div',
        { 
          'data-type': id,
          'data-type': node.attrs.type,
          class: `callout callout-${node.attrs.type}`,
          title: node.attrs.title
        },
        ['div', { class: 'callout-title' }, node.attrs.title || '提示'],
        ['div', { class: 'callout-content' }, 0]
      ]
    },
    // 添加输入规则:/// 触发警告框
    inputRules: (nodeType) => [
      new InputRule(/^\/\/\/\s*(\w*)\s*(.*)$/, (state, match, start, end) => {
        const [_, type, title] = match;
        const attrs = {
          type: type || 'note',
          title: title || ''
        };
  
        return state.tr
          .replaceRangeWith(start, end, nodeType.create(attrs))
          .setSelection(state.selection.near(state.tr.doc.resolve(end)));
      })
    ],
    // 添加命令
    commands: (nodeType) => ({
      insertCallout: (attrs) => (state, dispatch) => {
        const node = nodeType.create(attrs);
        const transaction = state.tr.replaceSelectionWith(node);
        dispatch(transaction);
        return true;
      }
    })
  };
});
 
// 在编辑器中使用
Editor.make()
  .use(commonmark)
  .use(calloutNode)
  .create();

高级应用:协作编辑与性能优化

实时协作实现

milkdown通过collab插件支持基于Yjs的协作编辑:

import * as Y from 'yjs';
import { WebrtcProvider } from 'y-webrtc';
import { collab, ySyncPlugin } from '@milkdown/plugin-collab';
 
// 创建Yjs文档
const ydoc = new Y.Doc();
// 配置WebRTC提供者
const provider = new WebrtcProvider(
  'milkdown-collab-demo',  // 房间ID
  ydoc,                    // Yjs文档
  { params: { room: 'my-room' } }  // 自定义参数
);
 
// 创建共享类型
const yXmlFragment = ydoc.getXmlFragment('document');
 
Editor.make()
  .use(commonmark)
  .use(collab.configure((ctx) => {
    ctx.set(collab.config, {
      doc: ydoc,
      fragment: yXmlFragment,
      // 配置用户信息
      user: {
        name: '用户' + Math.floor(Math.random() * 1000),
        color: `hsl(${Math.random() * 360}, 70%, 60%)`
      }
    });
  }))
  .select('#editor')
  .create();
 
// 监听连接状态
provider.on('status', (event) => {
  console.log('协作状态:', event.status);
  if (event.status === 'connected') {
    showNotification('已连接到协作会话');
  }
});

协作编辑架构:

image.webp

性能优化策略

大型文档优化技巧:

// 1. 启用文档分块渲染
Editor.make()
  .use(commonmark)
  .configure((ctx) => {
    ctx.set(editorViewOptionsCtx, {
      // 仅渲染可视区域附近的内容
      nodeViews: {
        // 为大型节点实现延迟加载
        code_block: (node, view, getPos) => {
          const dom = document.createElement('div');
          dom.className = 'code-block lazy-load';
    
          // 初始只显示占位符
          dom.innerHTML = '<div class="loading">加载代码块...</div>';
    
          // 延迟加载实际内容
          setTimeout(() => {
            // 实际渲染逻辑
            dom.innerHTML = `<pre><code>${node.textContent}</code></pre>`;
          }, 100);
    
          return { dom };
        }
      }
    });
  })
  // 2. 限制历史记录大小
  .use(history.configure({
    maxHistoryLength: 50  // 限制历史记录为50步
  }))
  // 3. 禁用不必要的输入规则
  .use(commonmark.configure((ctx) => {
    ctx.update(commonmark.config, config => ({
      ...config,
      inputRules: config.inputRules.filter(rule => 
        // 只保留必要的输入规则
        ['heading', 'list', 'code_block'].includes(rule.nodeType)
      )
    }));
  }))
  .create();

性能监控与分析:

// 监控编辑器性能
Editor.make()
  .use(commonmark)
  .use(Plugin.create('performance-monitor', (ctx) => {
    let lastUpdateTime = 0;
  
    return {
      view: (view) => {
        return {
          update: (prevState) => {
            const now = performance.now();
            const duration = now - lastUpdateTime;
      
            // 记录长时更新
            if (duration > 50) {  // 超过50ms的更新视为慢更新
              console.warn(`Slow update detected: ${duration.toFixed(2)}ms`);
              // 分析状态变化
              const transactions = view.state.transactions;
              transactions.forEach(tr => {
                if (tr.docChanged) {
                  console.log('Document changes:', tr.steps.length, 'steps');
                }
              });
            }
      
            lastUpdateTime = now;
          }
        };
      }
    };
  }))
  .create();

框架集成:React组件封装

import React, { useRef, useEffect, useState } from 'react';
import { Editor, EditorInstance } from '@milkdown/core';
import { commonmark } from '@milkdown/preset-commonmark';
import { crepe } from '@milkdown/crepe';
import { useEditor } from '@milkdown/react';
 
interface MilkdownEditorProps {
  defaultValue?: string;
  onChange?: (value: string) => void;
  className?: string;
}
 
export const MilkdownEditor: React.FC<MilkdownEditorProps> = ({
  defaultValue = '',
  onChange,
  className = ''
}) => {
  const editorRef = useRef<EditorInstance | null>(null);
  const [loading, setLoading] = useState(true);
  
  const render = useEditor((root) => {
    return Editor.make()
      .use(commonmark)
      .use(crepe)
      .select(root)
      .whenReady((editor) => {
        editorRef.current = editor;
        setLoading(false);
  
        // 设置初始内容
        if (defaultValue) {
          editor.setContent(defaultValue);
        }
  
        // 监听内容变化
        const unsubscribe = editor.onUpdate(() => {
          editor.getContent().then(content => {
            onChange && onChange(content);
          });
        });
  
        return unsubscribe;
      });
  });
  
  return (
    <div className={`milkdown-editor ${className}`}>
      {loading && <div className="loading">初始化编辑器中...</div>}
      <div ref={render} />
    </div>
  );
};
 
// 使用组件
const App = () => {
  const [content, setContent] = useState('');
  
  return (
    <div className="app">
      <h1>React Milkdown编辑器</h1>
      <MilkdownEditor
        defaultValue="# 开始编辑..."
        onChange={setContent}
        className="editor-container"
      />
      <div className="preview">
        <h2>预览</h2>
        <pre>{content}</pre>
      </div>
    </div>
  );
};

生态系统与未来展望

核心插件矩阵

milkdown生态提供丰富的官方插件:

插件类别 核心插件 功能描述
基础功能 commonmark 基础Markdown语法支持
扩展语法 gfm GitHub Flavored Markdown
编辑体验 history 撤销/重做历史
编辑体验 tooltip 悬浮提示与工具条
编辑体验 slash 命令菜单与自动补全
内容元素 table 表格编辑支持
内容元素 emoji 表情符号选择器
内容元素 image 图片上传与预览
协作功能 collab 基于Yjs的协作编辑
代码支持 prism 代码高亮与语法提示
交互增强 clipboard 增强剪贴板操作
交互增强 listener 文档事件监听

主题系统

milkdown提供灵活的主题系统,官方主题包括:

  • crepe:轻量级基础主题
  • nord:基于Nord配色方案的现代主题
  • frame:简洁框架风格主题

自定义主题示例:

import { Theme, createTheme } from '@milkdown/core';
import { nord } from '@milkdown/theme-nord';
 
const myTheme = createTheme((utils) => {
  // 继承nord主题
  const baseTheme = nord(utils);
  
  return {
    ...baseTheme,
    // 自定义CSS变量
    cssVars: {
      ...baseTheme.cssVars,
      primary: '#4f46e5',       // 主色调:靛蓝色
      secondary: '#10b981',     // 辅助色:绿色
      neutral: '#1f2937',       // 中性色:深灰
      'neutral-content': '#f9fafb' // 中性内容色:浅灰
    },
    // 自定义组件样式
    components: {
      ...baseTheme.components,
      button: {
        className: 'my-button',
        // 自定义渲染函数
        render: (props) => {
          return utils.h('button', {
            className: `my-button ${props.className}`,
            style: { backgroundColor: utils.cssVar('primary') },
            ...props.attrs
          }, props.children);
        }
      }
    }
  };
});
 
// 使用自定义主题
Editor.make()
  .use(myTheme)
  .create();

未来发展方向

milkdown团队计划在以下方向持续迭代:

  1. AI集成:内置AI辅助编辑功能,支持智能补全和格式优化
  2. 扩展生态:提供更多领域特定插件(绘图、公式、图表等)
  3. 性能优化:改进大型文档处理能力,支持百万字级文档流畅编辑
  4. 移动端适配:增强触摸交互支持,优化移动设备编辑体验
  5. 本地化支持:完善多语言包和RTL(从右到左)文本支持

结论:重新定义Markdown编辑体验

milkdown通过插件驱动架构,打破了传统编辑器的功能边界,为开发者提供了构建高度定制化编辑体验的能力。其核心优势在于:

  1. 极致灵活:从基础文本到复杂富媒体,从个人博客到企业协作系统,满足全场景需求
  2. 性能卓越:基于ProseMirror的高效文档模型,支持大型文档流畅编辑
  3. 易于扩展:完善的插件生态和详细的API文档,降低定制开发门槛
  4. 现代架构:TypeScript原生支持,与React/Vue等现代框架无缝集成

无论是构建简单的Markdown编辑器,还是开发复杂的富文本协作系统,milkdown都提供了坚实的基础和灵活的扩展能力。随着生态系统的不断完善,milkdown有望成为下一代富文本编辑技术的标准解决方案。