Milkdown Code Block Component 深度指南:基于 CodeMirror 的代码块组件配置与实战
2026/9/15 16:10:32 网站建设 项目流程

Milkdown Code Block Component 深度指南:基于 CodeMirror 的代码块组件配置与实战

【免费下载链接】milkdown🍼 Plugin driven WYSIWYG markdown editor framework.项目地址: https://gitcode.com/GitHub_Trending/mi/milkdown

导读

本文围绕 Milkdown 的codeBlockComponent组件展开,讲解如何在 WYSIWYG Markdown 编辑器中接入一个基于 CodeMirror 的代码块:包括完整的安装配置流程、全部 15 个配置项的含义与用法,以及语言加载、同步/异步预览、复制按钮等进阶能力的底层实现原理。读完本文,你将能够从零搭建一个带语言选择器、语法高亮、行号、代码补全/折叠、搜索替换乃至实时预览渲染的代码块组件,并理解其背后的 NodeView 与懒加载机制。


一、组件是什么:codeBlockComponent概览

codeBlockComponent是 Milkdown 官方提供的一个代码块节点组件,它用 CodeMirror 编辑器替换默认的纯文本代码块渲染,让编辑器内的代码块获得完整的编辑器体验。

根据 docs/api/component-code-block.md,该组件开箱即支持以下能力:

  • 语言选择器(Language picker)
  • 语法高亮(Syntax highlighting)
  • 行号显示(Line numbers)
  • 代码自动补全与折叠(Code auto-completion and folding)
  • 代码搜索与替换(Code search and replace)

注意:组件本身不提供任何样式milkdown-code-block等类名的外观(背景、边框、工具条布局等)需要你自己编写 CSS 来呈现。

组件在仓库中的组织方式

从源码结构看,该组件由两个 Milkdown 插件组成,位于 packages/components/src/code-block/index.ts:

export const codeBlockComponent: MilkdownPlugin[] = [ codeBlockView, codeBlockConfig, ]
  • codeBlockView:通过$viewcodeBlockSchema.node(来自@milkdown/preset-commonmark)绑定到自定义 NodeViewCodeMirrorBlock(见 packages/components/src/code-block/view/index.ts);
  • codeBlockConfig:通过$ctx暴露组件配置上下文codeBlockConfigCtx(见 packages/components/src/code-block/config.ts)。

也就是说,代码块的**数据层(ProseMirror 节点 schema)仍由 commonmark 预设提供,组件只负责视图层(NodeView)**的渲染与交互。


二、快速上手:完整配置示例

Editor.make()之后通过.config()更新codeBlockConfig上下文,并use(codeBlockComponent)挂载插件,即可启用组件。

import { defaultKeymap } from '@codemirror/commands' import { languages } from '@codemirror/language-data' import { oneDark } from '@codemirror/theme-one-dark' import { keymap } from '@codemirror/view' import { codeBlockComponent, codeBlockConfig, } from '@milkdown/components/code-block' import { defaultValueCtx, Editor } from '@milkdown/kit/core' import { commonmark } from '@milkdown/kit/preset/commonmark' import { basicSetup } from 'codemirror' await Editor.make() .config((ctx) => { ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, languages, extensions: [basicSetup, oneDark, keymap.of(defaultKeymap)], renderLanguage: (language, selected) => selected ? `✔ ${language}` : language, })) }) .use(commonmark) .use(codeBlockComponent) .create()

要点拆解:

  • ctx.update(codeBlockConfig.key, fn)是 Milkdown 上下文更新配置的标准写法,fn接收当前默认配置并返回新配置,务必展开...defaultConfig避免丢失其他默认项;
  • commonmark预设负责代码块节点的 schema、输入规则与快捷键,codeBlockComponent负责渲染;
  • basicSetup来自codemirror包,一次性开启行号、语法高亮、括号匹配等基础能力;
  • languages来自@codemirror/language-data,提供语言识别与懒加载支持。

另外,在 packages/plugins/preset-commonmark/src/node/code-block.ts 中可以看到配套的输入规则与快捷键:输入```javascript即可创建带语言标注的代码块(createCodeBlockInputRule),快捷键Mod-Alt-c可创建代码块(codeBlockKeymap),还有createCodeBlockCommandupdateCodeBlockLanguageCommand两个命令用于编程式创建/改语言。这意味着你既可以用鼠标点击语言选择器,也可以走命令管道动态设置语言。


三、配置项全解:15 个选项逐一说明

组件全部配置项都定义在CodeBlockConfig接口中(见 packages/components/src/code-block/config.ts),默认值见同文件defaultConfig。下表整理自文档并对照源码补充了默认值与类型细节:

选项类型默认值说明
extensionsExtension[][]CodeMirror 扩展列表
languagesLanguageDescription[][]CodeMirror 语言数据(用于语言选择器与高亮)
expandIconstring'⬇'展开语言选择器的图标
searchIconstring'🔍'搜索图标
clearSearchIconstring'⌫'清空搜索输入框的图标
searchPlaceholderstring'Search language'搜索输入框占位文本
noResultTextstring'No result'无匹配语言时显示的文本
copyTextstring'Copy'复制按钮文本
copyIconstring'📋'复制按钮图标
onCopy(text: string) => void(可选)() => {}复制成功后的回调
renderLanguage(language: string, selected: boolean) => string(language) => language渲染语言选择列表项,必须返回字符串
renderPreview(language, content, applyPreview) => void \| null \| string \| HTMLElement() => null渲染代码块预览:返回null隐藏预览,返回undefined触发异步渲染
previewToggleButton(previewOnlyMode: boolean) => string(mode) => mode ? 'Edit' : 'Hide'渲染预览切换按钮文本,必须返回字符串
previewLabelstring'Preview'预览面板标签
previewOnlyByDefaultboolean只读模式默认为true是否默认只显示预览
previewLoadingstring \| HTMLElement'Loading...'异步预览加载中的内容

源码中的previewOnlyByDefault是可选属性(previewOnlyByDefault?: boolean),组件内部通过props.config.previewOnlyByDefault ?? props.getReadOnly()求值,即未显式配置时,只读模式下默认进入纯预览态

下面按功能分组详细讲解各配置项的实战用法。

3.1languages:配置语言数据

languages是 CodeMirror 的LanguageDescription[]数组。你可以直接复用@codemirror/language-data的全量语言数据,也可以自定义精简的语言列表,以便控制包体积或只暴露你支持的语言:

import { LanguageDescription } from '@codemirror/language' import { languages } from '@codemirror/language-data' import { codeBlockConfig } from '@milkdown/components/code-block' const myLanguages = [ LanguageDescription.of({ name: 'JavaScript', alias: ['ecmascript', 'js', 'node'], extensions: ['js', 'mjs', 'cjs'], load() { return import('@codemirror/lang-javascript').then((m) => m.javascript()) }, }), LanguageDescription.of({ name: 'CSS', extensions: ['css', 'pcss'], load() { return import('@codemirror/lang-css').then((m) => m.css()) }, }), ] ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, languages: myLanguages, }))

从源码看,LanguageLoader(packages/components/src/code-block/view/loader.ts)会将所有语言的alias建立小写索引表,查找时先按languageName.toLowerCase()匹配 alias 或名称;命中后若language.support已缓存则直接返回,否则调用language.load()动态加载。这正是 CodeMirror 语言按需懒加载的实现位置:只有代码块真正使用了某种语言时,对应语言模块才会被 import。

3.2extensions:注入 CodeMirror 扩展

extensions是传给 CodeMirror 实例的扩展数组,用于定制编辑行为与主题。basicSetup已包含行号、语法高亮、自动补全等常用能力,可再叠加主题与按键:

import { defaultKeymap, indentWithTab } from '@codemirror/commands' import { oneDark } from '@codemirror/theme-one-dark' import { codeBlockConfig } from '@milkdown/components/code-block' import { basicSetup } from 'codemirror' ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, extensions: [ keymap.of(defaultKeymap.concat(indentWithTab)), basicSetup, oneDark, ], }))

注意:keymap需要从@codemirror/view导入(文档示例第一段即如此)。indentWithTab可以让 Tab 键在代码块内缩进而非移出焦点。

在 NodeView 源码(packages/components/src/code-block/view/node-view.ts)中可以看到,CodeMirror 实例创建时除了你配置的extensions,还会自动追加内部扩展:readOnlyConf(只读状态开关)、drawSelection()、内部按键映射、语言 Compartment,以及changeFilter(非编辑态下阻止用户编辑,但放行来自 ProseMirror 的同步更新)和updateListener(把 CodeMirror 的变更回写到 ProseMirror 文档)。

3.3renderLanguage:自定义语言列表项

用于在语言选择器中渲染每个语言项,selected表示当前项是否为代码块当前语言。必须返回字符串

ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, renderLanguage: (language, selected) => selected ? `✔ ${language}` : language, }))

在 language-picker.tsx 中,该函数返回值会作为列表项内容渲染;若返回undefined或空内容,列表项将无法正常展示,因此文档特别强调“Must return a string”。

3.4 图标与文案类选项:expandIcon/searchIcon/clearSearchIcon/copyIcon/copyText/searchPlaceholder/noResultText/previewLabel

这些选项全部是字符串,可以填任意文本或 emoji,用于本地化与个性化:

ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, expandIcon: '🔽', searchIcon: '🔍', clearSearchIcon: '❌', copyIcon: '📄', copyText: 'Copy code', searchPlaceholder: 'Find a language...', noResultText: 'No language found', previewLabel: 'Preview', }))

它们分别作用于语言选择器触发按钮、搜索框图标、清空按钮、复制按钮以及预览面板标签。在语言选择器源码中,搜索框的placeholder、清空按钮的显示条件(filter.value.length === 0时隐藏)以及“无结果”占位项的渲染都直接使用这些配置。

3.5onCopy:复制回调

点击复制按钮、代码成功写入剪贴板后触发:

ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, onCopy: (text) => { alert('Copied: ' + text) }, }))

从 copy-button.tsx 的源码可见,复制逻辑优先使用navigator.clipboard.writeText,失败时降级到隐藏textarea+document.execCommand('copy')的兼容方案(同时处理了 iOS 聚焦与选区恢复),复制成功后调用props.onCopy(props.text)


四、预览机制:renderPreview与相关配置

预览是代码块组件最具扩展性的能力——它允许你把代码内容渲染成真实产物(如 LaTeX 公式、编译后的 JS、Mermaid 图表等),在不离开编辑器的前提下看到结果。

4.1renderPreview:同步 / 异步 / 隐藏三种模式

函数签名:

renderPreview: ( language: string, content: string, applyPreview: (value: null | string | HTMLElement) => void ) => void | null | string | HTMLElement

其返回值约定:

  • 返回字符串或 HTMLElement:同步渲染预览;
  • 返回null:隐藏预览(不展示预览面板与切换按钮);
  • 返回undefined:进入异步渲染,先展示previewLoading,待计算完成后调用applyPreview(value)填入结果。
ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, renderPreview: (language, content, applyPreview) => { // 同步:LaTeX 内容直接渲染为 DOM if (language === 'latex' && content.length > 0) { return renderLatexToDOM(content) } // 异步:先显示 Loading,编译完成后 applyPreview if (language === 'JavaScript') { compileJs(content).then((res) => applyPreview(res)) return } // 隐藏预览 return null }, }))

从 code-block.tsx 源码看,watch监听textlanguage变化并重新调用renderPreview:有返回值则立即写入preview;返回undefined且当前无预览内容时,先把previewLoading经过DOMPurify 消毒后作为占位内容;返回null则清空预览。预览面板仅在preview.value存在时渲染,同时只有存在预览内容时才会显示切换按钮。

4.2 预览面板的安全处理:SVG-aware Sanitizer

预览内容会以innerHTML方式插入预览容器,因此安全性是硬要求。preview-panel.tsx 实现了一个针对 SVG 场景定制的 DOMPurify 消毒器:ADD_TAGS放行foreignObjectHTML_INTEGRATION_POINTS让其中 HTML 子节点正确解析,同时通过uponSanitizeElementhook 移除所有不在 SVG 命名空间内foreignObject,以规避 mXSS(如 CVE-2020-26870)风险。这意味着像 Mermaid v11+ 这类依赖 SVGforeignObject的图表可以安全渲染。

4.3previewToggleButtonpreviewOnlyByDefaultpreviewLoading

  • previewToggleButton:根据当前是否处于纯预览模式返回按钮文本,必须返回字符串
ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, previewToggleButton: (previewOnlyMode) => previewOnlyMode ? 'Show code' : 'Hide code', }))
  • previewOnlyByDefault:是否默认只显示预览。默认为只读模式下true(其他模式为false)。关闭纯预览后,编辑区与预览区会同时展示(中间有preview-divider分隔线):
ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, previewOnlyByDefault: false, }))
  • previewLoading:异步渲染期间的加载占位内容,可以是字符串或 HTMLElement:
ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, previewLoading: '<div>Loading...</div>', }))

五、深入原理:NodeView 如何把 CodeMirror 嵌入 ProseMirror

理解底层实现有助于你排查问题(如选区不同步、性能优化)或进一步扩展组件。核心实现集中在 packages/components/src/code-block/view/node-view.ts 的CodeMirrorBlock类。

5.1 双向数据同步

  • CodeMirror → ProseMirrorforwardUpdate监听 CodeMirror 的更新事件,将changes逐段映射到 ProseMirror 事务(tr.replaceWith/tr.delete),并同步设置文本选区;当 CodeMirror 未聚焦时不执行,避免无谓回写。
  • ProseMirror → CodeMirrorupdate(node)computeChange函数(前缀/后缀双指针求最小差异)计算出最小 change,再dispatch到 CodeMirror;当同步来源是 CodeMirror 自身(this.updating标记)时直接跳过。

5.2 语言与只读状态用 Compartment 动态切换

languageConfreadOnlyConf两个 Compartment 在编辑器创建时以空数组占位,之后语言切换(updateLanguage)与只读状态变化(update)都通过reconfigure动态注入,无需重建整个编辑器。

5.3 键盘交互与边界处理

内置按键映射(codeMirrorKeymap)实现了:

  • ArrowUp / ArrowLeft / ArrowDown / ArrowRight:光标位于代码块边界时,把焦点移出到外部段落(maybeEscape);
  • Mod-Enter:退出代码块并聚焦外部(调用exitCode);
  • Mod-z/Shift-Mod-z/Mod-y:在代码块内部执行 undo / redo;
  • Backspace:当光标位于首行行首且代码只有一行时,将代码块转换为普通段落。

5.4 性能优化:基于 IntersectionObserver 的懒加载与回收

CodeMirrorBlock使用共享的IntersectionObserverrootMargin: '200px')监听每个代码块容器的可见性:

  • 代码块进入视口(或接近视口)时才初始化 CodeMirror 实例,未初始化前先渲染一个静态<pre>占位;
  • 滚出视口 5 秒(TEARDOWN_DELAY = 5000)后自动销毁实例并恢复占位符,若此时用户正聚焦其中则跳过销毁;
  • 销毁/重建的逻辑保证滚动长文档时内存占用可控。

这意味着组件天然适合包含大量代码块的长文档,也是文档中“行号、高亮、补全”等能力不会拖慢初始渲染速度的原因。

5.5 语言选择器交互细节

language-picker.tsx 展示了选择器的完整交互逻辑:

  • 使用@floating-ui/domcomputePosition将下拉列表定位到触发按钮下方(placement: 'bottom-start');
  • 打开时自动聚焦搜索框;输入关键字会同时匹配语言namealias(大小写不敏感);
  • 当前选中的语言项始终置顶;无匹配时展示noResultText
  • 点击外部区域关闭下拉(通过window点击监听与data-expanded判断);只读模式下禁止展开。

六、样式说明与动手清单

组件只生成结构化 DOM 与类名,不包含任何视觉样式。需要你自己编写 CSS 的类名主要包括:

  • .milkdown-code-block:代码块整体容器(由 NodeView 创建);
  • .tools.tools-button-group:工具条区域;
  • .language-button.expand-icon.language-picker.search-box.language-list等:语言选择器相关;
  • .copy-button:复制按钮;
  • .codemirror-host:CodeMirror 挂载容器(预览纯模式时添加hidden类);
  • .preview-panel.preview-divider.preview-label.preview:预览面板;
  • .milkdown-code-block-placeholder:未初始化前的静态内容占位。

若想参考官方主题对代码块的整体视觉处理,可查看 packages/crepe/src/theme 下各主题样式文件中对code-block相关类名的定义;theme-nord包(packages/plugins/theme-nord)也提供了另一套可直接借鉴的配色方案。文档与 Storybook 演示(storybook/stories/components)中同样有配套样式可参考。

动手清单(建议按序验证)

  1. 安装依赖:@milkdown/components/code-blockcodemirror@codemirror/commands@codemirror/language-data@codemirror/theme-one-dark(如需深色主题);
  2. 按第二节示例完成基础接入,确认输入```js后出现带语言选择器的代码块;
  3. 依次验证:切换语言后高亮变化、行号与折叠、搜索替换、复制按钮与onCopy回调、只读模式下的previewOnlyByDefault行为;
  4. 配置renderPreview体验同步/异步/隐藏三种预览形态,并确认预览内容经过安全消毒;
  5. 编写 CSS 完成视觉定制,并开启多代码块长文档滚动测试懒加载回收效果。

七、小结

codeBlockComponent把 CodeMirror 的编辑能力与 ProseMirror 的文档模型无缝桥接:通过codeBlockConfig上下文你可以控制语言数据、编辑器扩展、图标文案、复制回调与整套预览机制;底层 NodeView 则负责双向同步、动态语言切换、焦点逃逸、IntersectionObserver 懒加载等复杂细节。它既适合直接开箱使用,也因配置面完整而易于深度定制,是 Milkdown 生态中体验与扩展性都相当完整的组件之一。

【免费下载链接】milkdown🍼 Plugin driven WYSIWYG markdown editor framework.项目地址: https://gitcode.com/GitHub_Trending/mi/milkdown

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询