Chromium 148 升核 PDF 白屏:Guest Console 分层诊断与全链路排查实录

Chromium 148 升核 PDF 白屏:Guest Console 分层诊断与全链路排查实录

本文记录一次典型的「升核后本地 PDF 白屏」问题,覆盖 Browser 进程 C++ 链路Renderer 进程前端 ES Module 链OOPIF 架构开关对齐,以及常被误解的「JS 注入」。
结论先行:本次 case 的主阻塞在定制 PDF 前端的 ES Module 运行期断裂;C++ 侧需按 132 开关对齐(关闭 OOPIF)并排除 embedder 模板冲突,但不能用 C++ 解释 customElements.get('pdf-viewer') === null
文中场景已脱敏,方法可复用到任何基于 Chromium、自带 PDF 扩展前端的 fork 项目。


目录

  1. 端到端架构:从打开 PDF 到像素上屏
  2. 132 开关对齐:关闭 OOPIF
  3. ES Module 到底是什么?算客户端还是前端?
  4. 「JS 注入」到底指什么?底层机制
  5. C++ 侧全链路排查
  6. 前端 ES Module 链排查(本次主阻塞)
  7. 两种白屏的分叉诊断
  8. 148 升核变化清单
  9. Checklist 与工具
  10. Plugin Embed 机制详解
  11. Guest Console 分层诊断实战

1. 端到端架构:从打开 PDF 到像素上屏

Chromium 打开本地 PDF 时,至少涉及 3 个进程 / 3 层代码

PDF 插件进程 / Content iframe(Renderer)

扩展 Guest 页(Renderer + V8)

Embedder 页(Renderer)

Browser 进程(C++)

postMessage / mimeHandlerPrivate

manifest + pak 资源

导航 / 下载

PluginResponseInterceptor
拦截 PDF 响应

Stream 管理器
OOPIF: PdfViewerStreamManager
非 OOPIF: MimeHandlerStreamManager

内置 PDF 扩展注册
pdf_extension_util

pdf_embedder.html 或旧 embed 模板

Shadow DOM + iframe

index.html

main.js → pdf_viewer_wrapper.js

pdf_viewer.js 及整棵 import 树

customElements.define pdf-viewer

createPlugin embed 元素

PDFium 渲染

关键分层:

层级进程语言职责
BrowserbrowserC++拦截 PDF、建 stream、选 embedder 模板、注册扩展、下发 loadTimeData
EmbedderrendererHTML/JS132 旧架构:全页 <embed>;148 OOPIF:shadow DOM + iframe 容器
扩展 Guestrenderer(扩展 origin)TS/JS ES Module360 工具栏 UI、pdf-viewer 组件、创建 plugin embed
PDF 内容renderer(插件/OOPIF content)C++/PDFium实际画 PDF 页面

白屏可能发生在任意一层,必须用分层手段定位,不能默认是 C++ 或默认是前端。


2. 132 开关对齐:关闭 OOPIF

2.1 为什么升核后要谈 OOPIF

148 上游引入 OOPIF PDF(Out-of-Process Iframe PDF):PDF 内容跑在独立 iframe / 进程,扩展 UI 跑在扩展 iframe,Browser 侧用 PdfViewerStreamManager + pdfViewerPrivate API。

132 时代默认是 GuestView + 全页 embed 模板,扩展用 mimeHandlerPrivate.getStreamInfo

Browser 走 OOPIF 路径,但 embedder 页仍是 132 旧 <embed> 模板,或 扩展 JS 仍只调 mimeHandlerPrivate,会出现:

  • 整页白、连定制工具栏都没有(扩展 Guest 根本没进正确 iframe)
  • 或工具栏在、中间白(UI 起来了,stream / plugin 握手失败)

对齐 132 的第一动作:在排查期关闭 OOPIF,让 Browser + Embedder + 扩展 API 回到 132 一致的三件套。

2.2 132 vs 148 开关对照

132(旧核)148 上游默认对齐 132 目标
Feature PdfOopif默认关Windows 等平台 默认开
Browser Stream 管理MimeHandlerStreamManagerPdfViewerStreamManager非 OOPIF 路径
扩展取 streammimeHandlerPrivate.getStreamInfopdfViewerPrivate.getStreamInfomimeHandlerPrivate
插件属性mimeHandlerPrivate.setPdfPluginAttributespdfViewerPrivate.setPdfPluginAttributesmimeHandlerPrivate
Embedder 模板全页 <embed internalid=…>IDR_PDF_EMBEDDER_HTML(iframe)旧 embed 模板
index.html无 OOPIF 标记$i18n{pdfOopifEnabled}pdfOopifEnabled 属性无该属性
manifest.jsonpdfViewerPrivatepdfViewerPrivate 权限可不依赖

2.3 在本 fork 里如何关 OOPIF

方式 A — Feature 默认值(推荐与 132 对齐)

pdf/pdf_features.cc 中,本 fork 可通过 USE_360HACK 将 OOPIF 默认关闭:

#if BUILDFLAG(IS_CHROMEOS)
BASE_FEATURE(kPdfOopif, base::FEATURE_DISABLED_BY_DEFAULT);
#else
#ifdef USE_360HACK
BASE_FEATURE(kPdfOopif, base::FEATURE_DISABLED_BY_DEFAULT);  // 对齐 132
#else
BASE_FEATURE(kPdfOopif, base::FEATURE_ENABLED_BY_DEFAULT);
#endif
#endif

方式 B — 用户 Pref

prefs::kPdfViewerOutOfProcessIframeEnabled = "pdf.enable_out_of_process_iframe_viewer"

Pref 为 false 且 policy 允许时,IsOopifPdfEnabled() 才为 false(还受 FeatureList 约束)。

方式 C — 启动参数(验证用)

--disable-features=PdfOopif

需确认未被 Field Trial 覆盖。

方式 D — C++ 注入 loadTimeData(与前端联动)

pdf_extension_util.ccGetCommonStrings()

dict.Set("pdfOopifEnabled",
         chrome_pdf::features::IsOopifPdfEnabled() ? "pdfOopifEnabled" : "");

index.html 中的 $i18n{pdfOopifEnabled} 在资源下发时被替换:OOPIF 关 → 空字符串,<html> pdfOopifEnabled 属性;扩展 JS 据此走 mimeHandlerPrivate 分支。

2.4 必须同步检查的 C++ 陷阱:embedder 模板优先级

即使 Feature 关了 OOPIF,若 mime_handler_view_attach_helper.ccUSE_360HACK 在 OOPIF 判断之前强制返回旧 PDF 模板,行为仍可能混乱:

#ifdef USE_360HACK
  if (mime_type == "application/pdf") {
    return base::StringPrintf(kPdfFullPageMimeHandlerViewHTML, ...);  // 132 全页 embed
  }
#endif

#if BUILDFLAG(ENABLE_PDF)
  if (chrome_pdf::features::IsOopifPdfEnabled() && mime_type == pdf::kPDFMimeType) {
    // IDR_PDF_EMBEDDER_HTML — OOPIF iframe 模板
  }
#endif

对齐 132 时: OOPIF 关 → 应走 kPdfFullPageMimeHandlerViewHTML 或通用 embed 模板,不应再要求扩展具备 OOPIF 的 pdfViewerPrivate / pdf_embedder.css 等 148 专属文件。

若将来要在 148 上正式启用 OOPIF: 必须把 OOPIF 分支放在 USE_360HACK 之前,或删除 HACK 的 unconditional 提前 return,否则 Browser 侧 OOPIF 与页面模板永远不一致。

2.5 运行时如何确认 OOPIF 状态

PDF 扩展 Guest 页 Console(不是外层 embedder 页):

// false = 对齐 132(非 OOPIF 前端分支)
document.documentElement.hasAttribute('pdfOopifEnabled')

// 看实际调用的 API
typeof chrome.pdfViewerPrivate   // OOPIF 扩展 manifest 才有
typeof chrome.mimeHandlerPrivate // 132 路径必有

Browser 侧日志 / 断点:

chrome_pdf::features::IsOopifPdfEnabled()
base::FeatureList::IsEnabled(chrome_pdf::features::kPdfOopif)

3. ES Module 到底是什么?算客户端还是前端?

3.1 定义

ES Module(ECMAScript Module) 是 JavaScript 语言标准的模块系统,使用:

<script type="module" src="main.js"></script>

或源码中的:

import './pdf_viewer.js';
export class PdfViewerElement { ... }

浏览器加载模块时会:

  1. Fetch 模块 URL(受 CSP、扩展 origin 约束)
  2. Parse 为 Module Record
  3. 递归解析 所有静态 import(构建依赖图)
  4. Instantiate + Evaluate(执行顶层代码,包括 customElements.define

任意一步失败 → 该模块及其 importer 链标记为 rejected。

3.2 「客户端」还是「前端」?

容易混用的三个词:

说法在本问题里指什么
前端扩展 Guest 页里的 TS/JS/HTML/CSS(pdf360 资源树)
客户端相对「服务端」而言;PDF 查看器没有服务端渲染,UI 全在浏览器里
Renderer / 渲染进程Chromium 多进程模型里跑 V8 和 Blink 的进程

准确表述:

ES Module 运行在 **Renderer 进程的扩展 Guest 页(前端 Web UI)**里,由 Blink 模块加载器 + V8 执行;不是 Browser 进程的 C++,也不是 PDF 插件进程里的 C++。

因此:

  • 排查 ES Module → DevTools Console、import()、Network(type=module
  • 排查 C++ stream / 模板 → Browser 断点、chrome://tracing、日志

不存在「C++ 客户端帮你执行 ES Module」;C++ 只负责把扩展资源放进 pak、Serving 时做 $i18n 替换、以及拦截 PDF 字节流。

3.3 与 classic script 的区别

Classic <script>ES Module
作用域全局模块作用域
依赖无静态依赖图静态 import
失败表现单文件报错,其它 script 可能仍执行入口模块 rejected,依赖它的全部失败
defer可选默认 defer
customElements.define若在失败链上游则 never runs同上

pdf360 的 index.html 使用 module

<script type="module" src="main.js"></script>
<script type="module" src="pdf_viewer_wrapper.js"></script>

main.jsimport './pdf_viewer_wrapper.js' → wrapper 再 export/import pdf_viewer.js → 数十个 side-effect import(toolbar、sidenav…)。

所以白屏时 customElements.get('pdf-viewer') === null 的直接含义是:pdf_viewer.js 那条 module 链没有完整 evaluate 到 customElements.define 那一行。

3.4 fetch 200 ≠ Module 成功

手段证明什么
fetch('pdf_viewer.js') → 200pak 里有这个文件
import(chrome.runtime.getURL('pdf_viewer.js'))文件 + 整棵静态 import 树均可 load & evaluate

Chrome 报错常写成:

Failed to fetch dynamically imported module: …/pdf_viewer.js

叶子模块(如 elements/icons.html.js)才是真正 first failure,DevTools 需展开 stack 或对叶子单独 import()


4. 「JS 注入」到底指什么?底层机制

排查中「JS 注入失败」常被误用来指 pdf_viewer.js 没加载。实际上 Chromium PDF 链路里存在 多种不同机制的「注入」,必须拆开。

4.1 类型对照表

说法实际机制谁做的是否涉及 pdf_viewer.js
响应体替换Browser 拦截 PDF HTTP 响应,把 body 换成 embedder HTMLC++ OverrideBodyForInterceptedResponse
HTML 占位符替换Serving 扩展 index.html 时替换 $i18n{…}C++ pdf_extension_util + WebUI 管道否(只改 HTML 属性/文本)
loadTimeData / 额外字典JS 里 loadTimeData.getString('…')C++ GetStrings / GetAdditionalData
扩展 ES Module 加载Guest 页 <script type="module"> 正常导航加载Blink 模块加载器是,但这是 Web 标准加载,不是 C++ 注入
Plugin embed 创建document.createElement('embed') + 属性前端 pdf_viewer_base.ts
第三方扩展 Content Scriptfile:// 等页注入浮动条其它扩展 content_scripts与白屏主因通常无关
DevTools caniuse-lite兼容性数据库里的文件名巧合完全无关

4.2 响应体替换(C++,打开 PDF 的第一步)

用户导航到 file:///…/doc.pdf 或下载完成后展示 PDF 时:

  1. BrowserPluginResponseInterceptor(或等价逻辑)识别 application/pdf
  2. 不直接把 PDF 字节交给 Blink 当 HTML 显示
  3. 调用 MimeHandlerViewAttachHelper::OverrideBodyForInterceptedResponse
  4. 生成 embedder 页 HTML 字符串(132:全页 embed;148 OOPIF:IDR_PDF_EMBEDDER_HTML
  5. HTML 作为导航响应 body 进入 Renderer

embed 标签携带 internalid,用于后续 stream 与 Guest 关联。

这是 HTML 字符串替换,不是往页面里 eval 一段 pdf_viewer.js。

4.3 扩展 Guest 如何进来

132 非 OOPIF 典型序列:

embedder 页加载(含 <embed type="application/pdf" internalid="…">)
  → MimeHandlerViewGuest  attach
  → Guest 导航到 chrome-extension://<pdf-ext-id>/index.html?…
  → index.html 里 <script type="module" src="main.js">
  → Blink 按 ES Module 规则拉取 main.js、pdf_viewer_wrapper.js、pdf_viewer.js…

148 OOPIF 典型序列:

embedder = pdf_embedder.html(shadow root + iframe)
  → iframe 先 about:blank,再导航到扩展 index.html
  → PdfViewerStreamManager claim stream
  → 扩展调用 pdfViewerPrivate.getStreamInfo
  → 另开 PDF content iframe 加载 PDF 字节

4.4 $i18n 替换(常被误认为「JS 注入」)

扩展 index.html 源码:

<html dir="$i18n{textdirection}" lang="$i18n{language}" $i18n{pdfOopifEnabled}>

Browser 在 打包资源经扩展协议下发前,用 pdf_extension_util::GetStrings() 等做字符串替换:

  • $i18n{pdfOopifEnabled}"pdfOopifEnabled"""
  • $i18n{textdirection}ltr / rtl

替换的是 HTML 文本,不会自动执行 JS。

4.5 前端创建 plugin embed(不是 C++ 注入 script)

pdf_viewer_base.tscreatePlugin_()

const plugin = document.createElement('embed');
plugin.id = 'plugin';
plugin.type = 'application/x-google-chrome-pdf';
plugin.setAttribute('original-url', this.originalUrl);
// OOPIF: pdfViewerPrivate.setPdfPluginAttributes
// 非 OOPIF: mimeHandlerPrivate.setPdfPluginAttributes

Browser 根据 embed 类型把 PDF 插件实例挂到进程模型里。这是 DOM 创建 + 私有 API 传参,不是 Browser 往 V8 里注入 pdf_viewer.js。

4.6 第三方扩展「注入」

白屏时若 DevTools Sources 里看到 file:// 页上有 AI 助手、划词等脚本,那是 其它扩展的 content_scripts,运行在外层 tab,不是 PDF Guest 内部,也 不会 替代 pdf-viewer 组件注册。


5. C++ 侧全链路排查

以下按 时间顺序 列出 Browser 进程关键节点、验证方法与常见失败点。

5.1 扩展注册与资源 pak

步骤代码位置(概念)验证
内置 PDF 扩展 manifestpdf_extension_util::GetManifest()IDR_PDF_MANIFEST扩展 ID 固定;定制版 manifest 来自 pdf360 打包
JS/CSS 进 pakBUILD.gngritchrome/browser/resources/pdf360fetch(chrome.runtime.getURL('main.js')) 在 Guest 页 200
字符串 / Feature 开关GetStrings() / GetAdditionalData()Guest 页 loadTimeData.getString('…') 有值;pdfAnnotationsEnabled 等 legacy key 存在

失败征象: Guest URL 404、扩展未加载、chrome-extension://… 无法访问 → 先查 pak 与 manifest,再谈 JS。

5.2 PDF 响应拦截

步骤说明
识别 MIMEapplication/pdf
创建 stream内部 stream id + internal_id
替换 bodyembedder HTML 写入导航响应

断点 / 日志关键词:

  • OverrideBodyForInterceptedResponse
  • CreateTemplateMimeHandlerPage

检查清单:

  • IsOopifPdfEnabled() 与 embedder 模板一致(132 关 OOPIF → 不应 serving OOPIF embedder 却又走 PdfViewerStreamManager 无 claim)
  • USE_360HACK PDF 分支是否 抢在 OOPIF 判断前 return 旧模板

5.3 Stream 管理器(OOPIF vs 非 OOPIF)

模式管理类扩展 API
OOPIF 开PdfViewerStreamManagerpdfViewerPrivate
OOPIF 关MimeHandlerStreamManager(legacy)mimeHandlerPrivate

OOPIF 关键回调(pdf_viewer_stream_manager.cc):

  • DidStartNavigation — PDF content iframe 导航开始
  • ReadyToCommitNavigation — embedder claim StreamInfoSetInternalId
  • DidFinishNavigation — 扩展 Guest 导航完成、postMessage setup

非 OOPIF 验证:

chrome.mimeHandlerPrivate.getStreamInfo(info => console.log(info));
// 应有 streamUrl、originalUrl、tabId

失败征象:

  • getStreamInfo 永不回调 / 空对象 → Browser stream 未 associate 到当前 Guest
  • 工具栏在、中间白 → 优先查 stream + plugin attributes + 插件进程

5.4 Embedder 与 Guest attach

MimeHandlerViewGuest / MimeHandlerViewEmbedder 负责:

  • Guest iframe 尺寸、visibility
  • 扩展 handler URL(manifest.mime_types_handlerindex.html

验证:

  • DevTools → 选中 扩展 origin 的 frame(chrome-extension://…/index.html
  • 若只有外层 file:// 或空白 embedder、没有 extension frame → attach / 模板错误

5.5 插件进程与 PDF 渲染

Guest UI 起来后,PluginController 通过 embed / postMessage 与 PDF 引擎通信。

C++ 相关:

  • PluginService::GetPluginInfoArray — 插件注册、MIME 与路径
  • PDF 插件进程 crash → 中间区域白、但工具栏可能仍在

验证:

  • chrome://plugins / 内部 plugin list(视版本而定)
  • Guest 页是否存在 #plugin 或 OOPIF content iframe
  • PluginController.getInstance().pluginReady_(前端调试用)

5.6 C++ 排查决策树(简版)

打开 PDF
├─ 无 chrome-extension:// frame
│   ├─ 查 OverrideBody / CreateTemplateMimeHandlerPage / OOPIF 与 USE_360HACK 顺序
│   └─ 查 MimeHandler attach、扩展是否注册
├─ 有 extension frame,但 main.js 404
│   └─ 查 grit/pak/BUILD.gn 资源列表
├─ extension frame 有 UI 元素但 customElements.get('pdf-viewer') 假
│   └─ 【转前端】ES Module 链(第 6 节),不是 C++ 注入 pdf_viewer.js
└─ pdf-viewer 已注册,中间仍白
    ├─ getStreamInfo / setPdfPluginAttributes 是否成功
    ├─ OOPIF 开关与 browser_api 分支是否一致
    └─ 插件进程 / PDF content 导航

6. 前端 ES Module 链排查(本次主阻塞)

C++ 链路正常(Guest 能开、mimeHandlerPrivate 有 stream、main.js 200)的前提下,本次 case 阻塞于 module evaluate。

6.1 入口与 bisect 顺序

// 必须在 PDF Guest 页执行
console.log(location.href);
console.log(!!customElements.get('pdf-viewer'));

import(chrome.runtime.getURL('pdf_viewer.js'))
  .then(() => console.log('OK', !!customElements.get('pdf-viewer')))
  .catch(e => console.error('FAIL', e.message));
阶段结果含义
pdf_viewer_base.js 及基础依赖stream、controller、viewport 链 OK
pdf_viewer.jsUI 侧 effect import 有问题
elements/viewer-pdf-sidenav.js第一个 UI 断点
elements/icons.html.js148 iron-iconset-svg / 重复 name="pdf"
icons 修好后✅ sidenav 等
elements/viewer-toolbar.js当前主阻塞
整链 pdf_viewer.js待 toolbar 子树 bisect

6.2 icons.html — 148 WebUI 换代(已证实)

  • 旧:iron-iconset-svg import 404 或行为异常
  • 重复 <cr-iconset name="pdf">Tried to add a second iconset with id 'pdf'

6.3 viewer-toolbar.js — 待 bisect

  • 约 90+ 模块,文件均在 pak
  • 失败为 runtime / 传递依赖,需对 chrome:// 与 elements 子模块逐个 import()
  • 高嫌疑:viewer-download-controls.js$i18n)、viewer-annotations-bar.js(ink2 默认开)、viewer-toolbar-stamp.js

6.4 编译链(编不过则 Guest 拿不到新 JS)

问题说明
viewer-ink-host.ts / InkApi.canUndo148 类型无此方法
ArrayBuffer / Uint8Array.bufferTS 更严
promise_resolver.jsexport class vs export var X = class
ESLint type-aware可临时 enable_type_aware_eslint_checks = false

6.5 Release rollup

Debug 散列便于 bisect;Release 若 shared.rollup.js 未进 pak → wrapper 404,与 ESM 叶子 404 症状类似,需单独查 Network。


7. 两种白屏的分叉诊断

现象优先查
整页白,无定制工具栏,仅 tab 标题C++ embedder / Guest 未进扩展OOPIF vs 模板、USE_360HACK 顺序、extension frame 是否存在
工具栏在,中间白,页码可能有stream / 插件 / OOPIF API 不一致getStreamInfo、setPdfPluginAttributes、插件进程
连工具栏都没有,但 Guest Console 能开、fetch main.js 200前端 ES Moduleimport('pdf_viewer.js')customElements.get('pdf-viewer')
只剩第三方浮动条其它扩展 content script忽略;回到 PDF Guest frame

8. 148 升核变化清单

定制 PDF 前端(132 时代代码)
        ↓ 升核
148:OOPIF 默认、WebUI cr-iconset、ink2 默认、TS/ESLint、rollup、loadTimeData/i18n
        ↓
未对齐开关 → C++ 模板/API 分叉
未对齐前端 → import 链 404 / assert
        ↓
白屏(多种外观,同一类「链路未完成」)

132 对齐策略(排查期):

  1. PdfOopif(Feature + 验证 pdfOopifEnabled 属性 absent)
  2. 确认 embedder 走 132 embed 模板
  3. 扩展 browser_apimimeHandlerPrivate
  4. 再 bisect 前端 ESM

148 正式适配策略(产品方向):

  1. 开 OOPIF,修 USE_360HACK 与 OOPIF 模板优先级
  2. index.html + manifest + pdfViewerPrivate + dual-path API
  3. 同步修 ESM / icons / toolbar / BUILD

10. Plugin Embed 机制详解(132 非 OOPIF,前三步已通时)

你已确认 Browser AddStream → Guest(index.html) → pdf-viewer 走 createPlugin 均 OK,但 plugin embed 没起来
这一节专门讲第 3 步之后、第 4 步(Plugin 进程加载 PDF)之前,Guest 内第二个 <embed> 如何被创建、如何被 Browser 拦截、如何与 PDFium 握手

10.1 先分清「三个 embed / frame」

132 非 OOPIF 下,同一次打开 PDF 其实叠了三层容器,名字都叫 embed,职责完全不同:

层级所在 frame谁创建type / 作用
L0 外层 embedderTab 主文档(file:// 被替换后的 HTMLBrowser CreateTemplateMimeHandlerPageapplication/pdf + internalid挂载扩展 Guest
L1 扩展 UIchrome-extension://…/index.html扩展 <pdf-viewer>工具栏、侧栏、viewport DOM
L2 内层 plugin embed仍在扩展 Guest 文档内 #content前端 createPlugin_()application/x-google-chrome-pdf + src=streamUrl挂载 PDF 插件子 frame
L3 插件内容页plugin embed 的 content frameBrowser PluginResponseWriter 生成 HTML内含又一个 <embed src=streamUrl> + pdf_internal_plugin_wrapper.js

「plugin embed 没起来」 通常指 L2 未进 DOM / 未导航 / 未握手,或 L3 的 Browser 拦截失败MapToOriginalUrl 返回空),而不是 L0 或 L1 的问题。

Tab (L0 embedder HTML)
 └── Guest: index.html + pdf-viewer (L1 UI)
       └── #content
             └── <embed id="plugin" src="streamUrl">  ← L2,前端 createPlugin_
                   └── 插件子 frame (L3,C++ PluginResponseWriter 注入的 HTML)
                         └── <embed src="streamUrl"> + wrapper.js
                               └── PDFium 进程 / 渲染

10.2 前端:从 initInternal 到 L2 进 DOM

时序(pdf_viewer_base.ts + controller.ts + viewport.ts):

main.js
  → createBrowserApi() → mimeHandlerPrivate.getStreamInfo  ✓(你已确认)
  → initViewer → pdf-viewer.init(browserApi)
  → initInternal(...)
       1. new Viewport(scroller, sizer, content)
       2. plugin_ = createPlugin_()          ← 只 createElement,尚未挂 DOM
       3. PluginController.init(plugin, viewport, ...)
            → viewport.setContent(plugin)
            → viewport.setRemoteContent(plugin)
                 → attachContent_: content_.appendChild(plugin)  ← L2 进 DOM,开始导航
createPlugin_() 做了什么
const plugin = document.createElement('embed');
plugin.id = 'plugin';                                    // 固定 id,打印等 C++ 会查
plugin.type = 'application/x-google-chrome-pdf';         // 触发 PDF 插件 MIME 路径
plugin.setAttribute('original-url', this.originalUrl);
plugin.src = streamInfo.streamUrl;                       // setPluginSrc:关键 URL
plugin.setAttribute('background-color',);
plugin.setAttribute('javascript', 'allow'|'block');
plugin.toggleAttribute('full-frame', true);              // 非 embedded 时
plugin.toggleAttribute('pdf-viewer-update-enabled', true);

// ★ Browser 侧 StreamContainer 上记插件加载参数(见 10.3)
chrome.mimeHandlerPrivate.setPdfPluginAttributes({
  backgroundColor:,
  allowJavascript:,
});

注意调用顺序: setPluginSrc(设 src)与 setPdfPluginAttributes 都在 appendChild 之前同步完成;真正触发导航一般在 L2 被 append 到 #content 之后

PluginController.init 的桥接准备

在 L3 的 connect 到来之前,postMessage 是** stub**:

this.plugin_.postMessage = (message, transfer) => {
  this.delayedMessages_!.push({message, transfer});  // 先排队
};
this.plugin_.addEventListener('message', e => this.handlePluginMessage_(e));

L3 wrapper 通过 window.parent.postMessage({type:'connect', token: streamUrl}, …, [port]) 连上后,bindMessageHandler(port) 才把真实 MessagePort.postMessage 换上去并 flush 队列。

10.3 Browser:setPdfPluginAttributes 是 L2→L3 的「通行证」

扩展调用 mimeHandlerPrivate.setPdfPluginAttributes → Browser MimeHandlerServiceImpl::SetPdfPluginAttributes → 写入 StreamContainer::pdf_plugin_attributes_

当 L2 的 src=streamUrl 触发导航时,ChromePdfStreamDelegate::MapToOriginalUrl 会校验:

if (stream->extension_id() != kPdfExtensionId ||
    stream->stream_url() != stream_url ||
    !stream->pdf_plugin_attributes()) {   // ★ 缺这个直接失败
  return std::nullopt;
}

任一条件不满足 → 不生成插件 HTML → L3 frame 空白 → 你看不到 loadProgress / documentDimensions → 中间区域永远白。

常见失败:

条件典型原因
!pdf_plugin_attributes()createPlugin_ 未跑到 / API 不存在 / setPdfPluginAttributes 在 OOPIF 分支用错 API
stream_url != navigation_urlsetPluginSrc 用了错 URL;getStreamInfo 与 embed src 不一致
extension_id 不匹配非内置 PDF 扩展的 stream

通过后 Browser 组装 PdfStreamDelegate::StreamInfo,其中:

info.injected_script = IDR_PDF_PDF_INTERNAL_PLUGIN_WRAPPER_ROLLUP_JS;  // wrapper 脚本
info.background_color / allow_javascript / full_frame = 来自 pdf_plugin_attributes

10.4 Browser:PluginResponseWriter 生成 L3 页面(真正的「插件页」)

components/pdf/browser/plugin_response_writer.cc 把 L2 导航响应 body 换成一段 固定 HTML 模板(不是 PDF 字节流):

<div id="sizer"></div>
<embed type="application/x-google-chrome-pdf" src="$streamUrl" original-url=""
       background-color="" javascript="" full-frame >
<script type="module">
  /* pdf_internal_plugin_wrapper.js 内容 inline 在这里 */
</script>

要点:

  • L3 的 embed 才是真正挂 PDFium 的节点;L2 的 embed 只是「插件容器」。
  • injected_script(wrapper)负责 MessageChannel,把 L3 与 L1 Guest 的 PluginController 连起来。
  • 这不是「C++ 往 Guest 里注入 pdf_viewer.js」,而是 仅对 plugin 子 frame 的 navigation 响应做 HTML 替换

10.5 pdf_internal_plugin_wrapper.jsconnect 握手

wrapper 在 L3 执行(pdf_internal_plugin_wrapper.ts 编译产物):

const channel = new MessageChannel();
const plugin = document.querySelector('embed')!;

// L3 embed → 经 port 转发到 Guest
plugin.addEventListener('message', e => channel.port1.postMessage(e.data));

// Guest → L3 embed
channel.port1.onmessage = e => plugin.postMessage(e.data);

// ★ 向扩展 Guest(parent)发起连接
window.parent.postMessage(
    {type: 'connect', token: srcUrl.href}, parentOrigin, [channel.port2]);

Guest 侧 pdf_viewer_base.handleScriptingMessage

if (message.data.type === 'connect') {
  if (message.data.token === this.browserApi.getStreamInfo().streamUrl) {
    PluginController.getInstance().bindMessageHandler(message.ports[0]);
  }
}

token 必须严格等于 streamUrl(含 query)。不一致则 connection-deniedpostMessage 永远走 stub 队列,插件消息全丢。

bindMessageHandler 之后:

this.plugin_.postMessage = port.postMessage.bind(port);
port.onmessage = e => this.handlePluginMessage_(e);
// flush delayedMessages_

10.6 加载完成信号:从 Plugin 到 UI

握手成功后,PDFium 侧通过 postMessage 上报(Guest 的 handlePluginMessage 处理):

消息 type含义
loadProgress0–100;100 时 setLoadState(SUCCESS);-1 失败弹错
documentDimensions页宽高 → viewport 布局
metadata标题、页数等
getPassword加密 PDF

PluginController 还会向插件发 {type:'viewport', zoom, xOffset, yOffset, …};插件 DoPaint 出图后通过 gesture/scroll 等消息回传。

若 L2/L3 未建立,这些 message 都不会来,页码可能卡在默认、中间全白。

10.7 「plugin embed 没起来」排查清单

注意: #plugin<pdf-viewer> 的 Shadow DOM 内,不要用 document.querySelector('#plugin') 在 document 根上查。
完整分层诊断流程见 §11 Guest Console 分层诊断实战

扩展 Guest 页 Console 执行(Shadow 正确写法):

const v = document.querySelector('pdf-viewer');
const root = v?.shadowRoot;
const plugin = root?.querySelector('#plugin') || root?.querySelector('#content embed');

console.log({
  hasShadow: !!root,
  scroller: !!root?.querySelector('#scroller'),
  content: !!root?.querySelector('#content'),
  pluginInShadow: !!plugin,
  pluginSrc: plugin?.src,
  streamUrl: v?.['browserApi']?.getStreamInfo?.()?.streamUrl,
});

// 监听 connect 是否被拒绝
window.addEventListener(‘connection-denied-for-testing’, () =>
console.error(‘connect token mismatch’));


DevTools **Frames** 面板应看到:

1. 扩展 Guest(`chrome-extension://…/index.html`)
2. **plugin 子 frame**(URL 常为 streamUrl 或 blob/about,取决于阶段)

Network(Guest 页、Preserve log):

- L2 append 后是否有对 **streamUrl** 的 document 请求
- 该 document 响应 body 是否为 **HTML embed 模板**(非 raw PDF)

Browser 断点 / 日志:

| 位置 | 看什么 |
|------|--------|
| `MimeHandlerServiceImpl::SetPdfPluginAttributes` | 是否被调用;`stream_` 是否有效 |
| `ChromePdfStreamDelegate::MapToOriginalUrl` | 是否因 `!pdf_plugin_attributes()` 提前 return |
| `PluginResponseWriter::GenerateResponse` | 是否生成 L3 HTML |

前端断点:

| 位置 | 看什么 |
|------|--------|
| `createPlugin_` | embed 属性、`setPdfPluginAttributes` 是否执行 |
| `Viewport.attachContent_` | `#content` 下是否有 embed |
| `handleScriptingMessage` `connect` | token 与 streamUrl 是否一致 |
| `bindMessageHandler` | delayedMessages 是否 flush |

### 10.8 与 OOPIF 的差异(148 开 OOPIF 时)

OOPIF 下 **不再** 在 Guest 里 create L2 embed;改为 `pdfViewerPrivate.setPdfPluginAttributes` + 独立 **PDF content iframe** 加载 `originalUrl` 字节流,UI 与内容进程分离。

132 对齐(OOPIF 关)时必须走 **本节 L2/L3 + mimeHandlerPrivate + PluginResponseWriter** 路径;若 Browser 已 OOPIF 而前端仍 createPlugin embed,或反之,会出现「Guest/UI 有、plugin 永远起不来」。

### 10.9 小结:plugin embed 本质

```text
L2 embed = 前端 DOM 节点 + streamUrl 导航请求
L3 HTML  = Browser 对这次导航的「HTML 注入」(PluginResponseWriter)
握手     = wrapper connect + MessagePort 替换 stub postMessage
渲染     = PDFium 在 L3 内 embed 上 DoPaint,经 port 与 Guest viewport 同步

前三步 OK 但 plugin 不起 → 先按 §11 确认 Guest 是否完成 init / Polymer 升级,再查 L2/L3、setPdfPluginAttributesMapToOriginalUrlconnect


11. Guest Console 分层诊断实战

本节记录一次真实排查:Browser 侧 AddStreamgetStreamInfo 均正常,但中间仍白屏。说明问题不在 C++ plugin embed 阶段,甚至还没走到 createPlugin_() / setPdfPluginAttributes / MapToOriginalUrl

11.1 诊断目标:先定「卡在哪一层」

Browser AddStream / getStreamInfo     ← C++ stream 是否通
        ↓
Guest main.js → initViewer → init()   ← 扩展入口是否跑完
        ↓
Polymer 升级 → shadowRoot + 模板     ← pdf-viewer 是否真实例化
        ↓
initInternal → createPlugin_()        ← L2 embed 是否进 Shadow
        ↓
setPdfPluginAttributes / MapToOriginalUrl  ← C++ plugin 拦截
        ↓
connect 握手 → PDFium 渲染

规则: 上层未完成时,不要先改下层。例如 hasShadow === false 时改 PluginResponseWriter 无效。

11.2 第一轮脚本(易误判,仍建议跑)

扩展 Guestchrome-extension://…/index.html)Console:

console.log('href', location.href);
console.log('5 ce', !!customElements.get('pdf-viewer'));
console.log('6 el', !!document.querySelector('pdf-viewer'));
console.log('7 viewer', !!window.viewer);
console.log('8 browserApi', !!window.viewer?.browserApi);
console.log('9 mime', typeof chrome.mimeHandlerPrivate?.getStreamInfo);

if (window.viewer) {
  try {
    console.log('10 stream', window.viewer.browserApi?.getStreamInfo?.());
  } catch (e) {
    console.log('10 err', e.message);
  }
}

chrome.mimeHandlerPrivate?.getStreamInfo?.(info => console.log('11 getStreamInfo', info));
如何读第一轮输出
输出含义
pluginInDom false(若在 document 根查 #plugin可能假象#plugin 在 Shadow DOM 内,document 级查不到
streamUrl undefined(经 window.viewer?.browserApi可能假象browserApi 为 protected,Console 点语法读不到;或 window.viewer陈旧引用
srcMatch true 且两边都是 undefined无参考价值undefined === undefined
11 getStreamInfo 有完整 streamUrlC++ / Browser stream 正常,Guest 与 stream 已关联
5 ce true + 7 viewer true + 8 browserApi false不能直接结论「没 init」;需 §11.3 Shadow 脚本 + §11.4 stale 检测

11 有 stream,但 Shadow 里无 #plugin → 卡在 Guest 前端 init 链,不是 L2/L3 C++ 拦截。

11.3 第二轮脚本(决定性:Shadow + init 状态)

const v = document.querySelector('pdf-viewer');
const root = v?.shadowRoot;

console.log('--- shadow ---');
console.log('hasShadow', !!root);
console.log('scroller', !!root?.querySelector('#scroller'));
console.log('content', !!root?.querySelector('#content'));
console.log('plugin', !!root?.querySelector('#plugin'));
console.log('pluginSrc', root?.querySelector('#plugin')?.src);

console.log('--- init state ---');
console.log('originalUrl', v?.['originalUrl']);
console.log('browserApi', v?.['browserApi']);
console.log('streamUrl from api', v?.['browserApi']?.getStreamInfo?.()?.streamUrl);

同时:Console 面板 Filter: error,看打开 PDF 瞬间是否有红字(Polymer / $ / init)。

典型结论
hasShadoworiginalUrl / browserApi结论
false全 undefinedPolymer 未升级init() 未执行;不是 C++ plugin 阶段
true有值init 至少跑过一部分
true + #plugin 有 + pluginSrc === streamUrl进入 L2/L3;再查 Frames / MapToOriginalUrl / connect
true + #content 在、无 #plugininitInternalcreatePlugin_ 前异常

实战样例(白屏 case):

hasShadow false
originalUrl / browserApi / streamUrl from api → 全 undefined
11 getStreamInfo → 有 streamUrl(Browser OK)

当前 <pdf-viewer> 未完成 Polymer 升级 / main 初始化链未在当前 DOM 节点上完成。

11.4 常见陷阱

A. window.viewer 是陈旧引用

main.tsinitViewer 里赋值 window.viewer,重开 PDF 后 DOM 是新节点,旧引用可能仍 true 但已 disconnected:

const v = document.querySelector('pdf-viewer');
console.log('sameRef', v === window.viewer);
console.log('v.connected', v?.isConnected);
console.log('window.viewer.connected', window.viewer?.isConnected);
console.log('v.ctor', v?.constructor?.name);           // 期望 PdfViewerElement
console.log('v.shadow', !!v?.shadowRoot);
console.log('window.viewer.shadow', !!window.viewer?.shadowRoot);

sameRef === falsewindow.viewer.isConnected === false以后以 document.querySelector('pdf-viewer') 为准,勿信 window.viewer

B. customElements.get('pdf-viewer') === true ≠ 当前节点已升级

只说明类已 define。再查:

const v = document.querySelector('pdf-viewer');
console.log('proto', Object.getPrototypeOf(v)?.constructor?.name);
console.log('isPdfViewer', v instanceof customElements.get('pdf-viewer'));

proto 仍是 HTMLElementisPdfViewer === false → rollup / import 链在 define 前或 upgrade 时失败(看 Console 红字)。

C. Release 下 import('pdf_viewer.js') 404 不是 ESM 根因证据

Release 打包时 pdf_viewer.js 打进 pdf_viewer_wrapper.js rollup,pak 里没有独立 pdf_viewer.js

// 错误(Release 必 404)
import(chrome.runtime.getURL('pdf_viewer.js'));

// 诊断 Release 应用这个
import(chrome.runtime.getURL('pdf_viewer_wrapper.js'))
  .then(() => console.log('wrapper OK', !!customElements.get('pdf-viewer')))
  .catch(e => console.error('wrapper FAIL', e.message));

404 只说明「不要用 Debug 路径测 Release」,不能证明 ESM 正常。

D. main.js 链静默失败

main.ts 常见写法只有 .then(initViewer).catchcreateBrowserApi()contentSettings reject 时不会 init,也不会清 window.viewer

手动补跑 init(仅诊断):

import(chrome.runtime.getURL('browser_api.js')).then(async ({createBrowserApi}) => {
  await import(chrome.runtime.getURL('pdf_viewer_wrapper.js'));
  const api = await createBrowserApi();
  const v = document.querySelector('pdf-viewer');
  console.log('before init shadow', !!v?.shadowRoot);
  v?.init?.(api);
  console.log('after init shadow', !!v?.shadowRoot);
  console.log('plugin', !!v?.shadowRoot?.querySelector('#plugin'));
});
结果根因
手动 init 后出现 shadow + #pluginmain.js 链没走到 initViewer(Promise 失败或时序)
手动 init 仍无 shadowPolymer 模板 / rollup / $ 运行期错误

11.5 按结果分工(改 C++ 还是改前端)

Console / Shadow 现象改哪里
5 ce falsewrapper FAIL前端 ESM 链(icons / toolbar / rollup)
5 ce truehasShadow false,Console 有 Polymer/$ 红字前端:模板、Polymer、360 定制 init()
5 ce truehasShadow false,手动 init 可恢复前端:main.ts init 链 + .catch / 时序
11 getStreamInfo 空 / 不回调C++:AddStream 与当前 Guest frame 未 associate
11 有 stream,hasShadow true,Shadow 无 #plugin前端:initInternal / createPlugin_ 前异常
Shadow 有 #pluginpluginSrc === streamUrl,无 plugin 子 frameC++:setPdfPluginAttributesMapToOriginalUrlPluginResponseWriter
Shadow 有 #plugin,有子 frame,仍白屏C++ connect / PDFium 或 viewport 消息链

11.6 与「前三步已确认」的关系

日志里常写「AddStream OK、Guest 在、createPlugin 设计如此」,但 Runtime 第 3 步可能并未完成

  • 没有 hasShadow → Polymer 未升级
  • 没有 originalUrl / bracket 访问的 browserApiinitInternal 未跑
  • 没有 Shadow 内 #plugin → 不可能进入 C++ plugin embed

正确顺序: 先 §11.3 确认 Shadow + init → 再 §10 查 L2/L3 C++。

11.7 建议粘贴的 4 项(定责最小集)

排查结束时请保留这 4 项输出:

  1. sameRef / v.ctor / v.connected
  2. v instanceof customElements.get('pdf-viewer')
  3. 打开 PDF 时 第一条红色 error(完整文本)
  4. 手动 init 后:after init shadowplugin 是 true 还是 false

据此可区分:main 没 initPolymer/rollup 升级失败、还是 真进入 C++ plugin 阶段


9. Checklist 与工具

9.1 升核后必做

C++ / 开关

  • IsOopifPdfEnabled() 与产品预期一致(132 对齐 = false)
  • Guest 页 document.documentElement.hasAttribute('pdfOopifEnabled') 符合预期
  • CreateTemplateMimeHandlerPage 与 OOPIF 开关一致
  • mimeHandlerPrivate.getStreamInfopdfViewerPrivate.getStreamInfo 有正常回调

前端

  • Guest 页 customElements.get('pdf-viewer') 为 true
  • document.querySelector('pdf-viewer').shadowRoot 存在(§11)
  • Shadow 内 #scroller / #content / #plugin 符合预期
  • chrome.mimeHandlerPrivate.getStreamInfostreamUrl(§11 项 11)
  • Release:import('pdf_viewer_wrapper.js') 成功(勿测独立 pdf_viewer.js
  • icons / toolbar bisect 全绿
  • Debug / Release 编译通过,Release 无 rollup 404

9.2 工具函数

async function imp(f, chromeUrl = false) {
  const url = chromeUrl ? f : chrome.runtime.getURL(f);
  try {
    await import(url);
    console.log('OK', f);
  } catch (e) {
    console.error('FAIL', f, e.message);
    throw e;
  }
}

9.3 总结表

问题答案
白屏直接原因(本次)pdf-viewer 未注册
技术根因(本次)前端 ES Module 链断裂(icons → toolbar)
ES Module 跑在哪Renderer 扩展 Guest(前端),非 Browser C++
pdf_viewer.js 谁加载Blink ES Module 加载器, C++ 注入
「JS 注入」常指什么实际是 HTML 响应替换 / $i18n / 第三方 content script,易误解
132 对齐关键关 OOPIF + embed 模板 + mimeHandlerPrivate
C++ 何时优先查无 extension frame、getStreamInfo 失败、模板/OOPIF 不一致
前端何时优先查Guest 在、main.js 200、pdf-viewer 未 define
plugin embed 不起(UI 已有)先 §11 Shadow/init,再查 L2 #pluginsetPdfPluginAttributesMapToOriginalUrl
Guest Console 诊断§11:hasShadowgetStreamInfo(11)、勿用 document 查 #plugin、Release 用 wrapper
PluginResponseWriter仅 L3 子 frame 导航响应,不是 Guest 的 pdf_viewer.js

文档类型:团队排查笔记 / 技术博客(脱敏)
适用:Chromium fork、内置 PDF 扩展、定制 pdf360 类前端、132→148 升核

这里写自定义目录标题

欢迎使用Markdown编辑器

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

新的改变

我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:

  1. 全新的界面设计 ,将会带来全新的写作体验;
  2. 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
  3. 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
  4. 全新的 KaTeX数学公式 语法;
  5. 增加了支持甘特图的mermaid语法1 功能;
  6. 增加了 多屏幕编辑 Markdown文章功能;
  7. 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
  8. 增加了 检查列表 功能。

功能快捷键

撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G

合理的创建标题,有助于目录的生成

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

如何改变文本的样式

强调文本 强调文本

加粗文本 加粗文本

标记文本

删除文本

引用文本

H2O is是液体。

210 运算结果是 1024.

插入链接与图片

链接: link.

图片: Alt

带尺寸的图片: Alt

居中的图片: Alt

居中并且带尺寸的图片: Alt

当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。

如何插入一段漂亮的代码片

博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.

// An highlighted block
var foo = 'bar';

生成一个适合你的列表

  • 项目
    • 项目
      • 项目
  1. 项目1
  2. 项目2
  3. 项目3
  • 计划任务
  • 完成任务

创建一个表格

一个简单的表格是这么创建的:

项目Value
电脑$1600
手机$12
导管$1

设定内容居中、居左、居右

使用:---------:居中
使用:----------居左
使用----------:居右

第一列第二列第三列
第一列文本居中第二列文本居右第三列文本居左

SmartyPants

SmartyPants 是一个文本转换工具,主要功能是将普通的 ASCII 标点符号自动转换为更美观的印刷体标点符号。例如:

原始符号转换后说明
"引号"“引号”直引号变弯引号
'单引号'‘单引号’直单引号变弯单引号
--两个连字符变短破折号
---三个连字符变长破折号
...三个点变省略号

创建一个自定义列表

Markdown
Text-to- HTML conversion tool
Authors
John
Luke

如何创建一个注脚

一个具有注脚的文本。2

注释也是必不可少的

Markdown将文本转换为 HTML

KaTeX数学公式

您可以使用渲染LaTeX数学表达式 KaTeX:

Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n1)!nN 是通过欧拉积分

Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t   . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=0tz1etdt.

你可以找到更多关于的信息 LaTeX 数学表达式here.

新的甘特图功能,丰富你的文章

2014-01-07 2014-01-09 2014-01-11 2014-01-13 2014-01-15 2014-01-17 2014-01-19 2014-01-21 已完成 进行中 计划一 计划二 现有任务 Adding GANTT diagram functionality to mermaid
  • 关于 甘特图 语法,参考 这儿,

UML图表

可以使用UML图表进行渲染,例如下面产生的一个序列图:

王五 李四 张三 王五 李四 张三 李四想了很长时间, 文字太长了 不适合放在一行. 你好!李四, 最近怎么样? 你最近怎么样,王五? 我很好,谢谢! 我很好,谢谢! 打量着王五... 很好... 王五, 你怎么样?
  • 关于 UML图表 语法,参考 这儿,

流程图

链接

长方形

圆角长方形

菱形

  • 关于 Mermaid 语法,参考 这儿,

FLowchart流程图

我们依旧会支持flowchart.js的流程图语法:

Created with Raphaël 2.3.0 开始 我的操作 确认? 结束 yes no
  • 关于 Flowchart流程图 语法,参考 这儿.

导出与导入

导出

如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。

导入

如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。


  1. mermaid语法说明 ↩︎

  2. 注脚的解释 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ปรัชญา แค้วคำมูล

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值