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 项目。
目录
- 端到端架构:从打开 PDF 到像素上屏
- 132 开关对齐:关闭 OOPIF
- ES Module 到底是什么?算客户端还是前端?
- 「JS 注入」到底指什么?底层机制
- C++ 侧全链路排查
- 前端 ES Module 链排查(本次主阻塞)
- 两种白屏的分叉诊断
- 148 升核变化清单
- Checklist 与工具
- Plugin Embed 机制详解
- Guest Console 分层诊断实战
1. 端到端架构:从打开 PDF 到像素上屏
Chromium 打开本地 PDF 时,至少涉及 3 个进程 / 3 层代码:
关键分层:
| 层级 | 进程 | 语言 | 职责 |
|---|---|---|---|
| Browser | browser | C++ | 拦截 PDF、建 stream、选 embedder 模板、注册扩展、下发 loadTimeData |
| Embedder | renderer | HTML/JS | 132 旧架构:全页 <embed>;148 OOPIF:shadow DOM + iframe 容器 |
| 扩展 Guest | renderer(扩展 origin) | TS/JS ES Module | 360 工具栏 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 管理 | MimeHandlerStreamManager | PdfViewerStreamManager | 非 OOPIF 路径 |
| 扩展取 stream | mimeHandlerPrivate.getStreamInfo | pdfViewerPrivate.getStreamInfo | mimeHandlerPrivate |
| 插件属性 | mimeHandlerPrivate.setPdfPluginAttributes | pdfViewerPrivate.setPdfPluginAttributes | mimeHandlerPrivate |
| Embedder 模板 | 全页 <embed internalid=…> | IDR_PDF_EMBEDDER_HTML(iframe) | 旧 embed 模板 |
index.html | 无 OOPIF 标记 | $i18n{pdfOopifEnabled} → pdfOopifEnabled 属性 | 无该属性 |
manifest.json | 无 pdfViewerPrivate | 需 pdfViewerPrivate 权限 | 可不依赖 |
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.cc 的 GetCommonStrings():
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.cc 里 USE_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 { ... }
浏览器加载模块时会:
- Fetch 模块 URL(受 CSP、扩展 origin 约束)
- Parse 为 Module Record
- 递归解析 所有静态
import(构建依赖图) - 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.js → import './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') → 200 | pak 里有这个文件 |
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 HTML | C++ 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 Script | 在 file:// 等页注入浮动条 | 其它扩展 content_scripts | 与白屏主因通常无关 |
| DevTools caniuse-lite | 兼容性数据库里的文件名巧合 | 无 | 完全无关 |
4.2 响应体替换(C++,打开 PDF 的第一步)
用户导航到 file:///…/doc.pdf 或下载完成后展示 PDF 时:
- Browser 侧
PluginResponseInterceptor(或等价逻辑)识别application/pdf - 不直接把 PDF 字节交给 Blink 当 HTML 显示
- 调用
MimeHandlerViewAttachHelper::OverrideBodyForInterceptedResponse - 生成 embedder 页 HTML 字符串(132:全页 embed;148 OOPIF:
IDR_PDF_EMBEDDER_HTML) - 该 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.ts 的 createPlugin_():
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 扩展 manifest | pdf_extension_util::GetManifest() → IDR_PDF_MANIFEST | 扩展 ID 固定;定制版 manifest 来自 pdf360 打包 |
| JS/CSS 进 pak | BUILD.gn → grit → chrome/browser/resources/pdf360 | fetch(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 响应拦截
| 步骤 | 说明 |
|---|---|
| 识别 MIME | application/pdf |
| 创建 stream | 内部 stream id + internal_id |
| 替换 body | embedder HTML 写入导航响应 |
断点 / 日志关键词:
OverrideBodyForInterceptedResponseCreateTemplateMimeHandlerPage
检查清单:
-
IsOopifPdfEnabled()与 embedder 模板一致(132 关 OOPIF → 不应 serving OOPIF embedder 却又走 PdfViewerStreamManager 无 claim) -
USE_360HACKPDF 分支是否 抢在 OOPIF 判断前 return 旧模板
5.3 Stream 管理器(OOPIF vs 非 OOPIF)
| 模式 | 管理类 | 扩展 API |
|---|---|---|
| OOPIF 开 | PdfViewerStreamManager | pdfViewerPrivate |
| OOPIF 关 | MimeHandlerStreamManager(legacy) | mimeHandlerPrivate |
OOPIF 关键回调(pdf_viewer_stream_manager.cc):
DidStartNavigation— PDF content iframe 导航开始ReadyToCommitNavigation— embedder claimStreamInfo,SetInternalIdDidFinishNavigation— 扩展 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_handler→index.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.js | ❌ | UI 侧 effect import 有问题 |
elements/viewer-pdf-sidenav.js | ❌ | 第一个 UI 断点 |
→ elements/icons.html.js | ❌ | 148 iron-iconset-svg / 重复 name="pdf" |
| icons 修好后 | ✅ sidenav 等 | |
elements/viewer-toolbar.js | ❌ | 当前主阻塞 |
整链 pdf_viewer.js | ❌ | 待 toolbar 子树 bisect |
6.2 icons.html — 148 WebUI 换代(已证实)
- 旧:
iron-iconset-svgimport 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.canUndo | 148 类型无此方法 |
ArrayBuffer / Uint8Array.buffer | TS 更严 |
promise_resolver.js | export 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 Module | import('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 对齐策略(排查期):
- 关
PdfOopif(Feature + 验证pdfOopifEnabled属性 absent) - 确认 embedder 走 132 embed 模板
- 扩展
browser_api走mimeHandlerPrivate - 再 bisect 前端 ESM
148 正式适配策略(产品方向):
- 开 OOPIF,修 USE_360HACK 与 OOPIF 模板优先级
index.html+ manifest +pdfViewerPrivate+ dual-path API- 同步修 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 外层 embedder | Tab 主文档(file:// 被替换后的 HTML) | Browser CreateTemplateMimeHandlerPage | application/pdf + internalid → 挂载扩展 Guest |
| L1 扩展 UI | chrome-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 frame | Browser 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_url | setPluginSrc 用了错 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.js:connect 握手
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-denied,postMessage 永远走 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 | 含义 |
|---|---|
loadProgress | 0–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、setPdfPluginAttributes、MapToOriginalUrl、connect。
11. Guest Console 分层诊断实战
本节记录一次真实排查:Browser 侧 AddStream、getStreamInfo 均正常,但中间仍白屏。说明问题不在 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 第一轮脚本(易误判,仍建议跑)
在 扩展 Guest(chrome-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 有完整 streamUrl | C++ / 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)。
典型结论
hasShadow | originalUrl / browserApi | 结论 |
|---|---|---|
| false | 全 undefined | Polymer 未升级 或 init() 未执行;不是 C++ plugin 阶段 |
| true | 有值 | init 至少跑过一部分 |
true + #plugin 有 + pluginSrc === streamUrl | 进入 L2/L3;再查 Frames / MapToOriginalUrl / connect | |
true + #content 在、无 #plugin | initInternal 在 createPlugin_ 前异常 |
实战样例(白屏 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.ts 在 initViewer 里赋值 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 === false 或 window.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 仍是 HTMLElement 或 isPdfViewer === 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)、无 .catch:createBrowserApi() 或 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 + #plugin | main.js 链没走到 initViewer(Promise 失败或时序) |
| 手动 init 仍无 shadow | Polymer 模板 / rollup / $ 运行期错误 |
11.5 按结果分工(改 C++ 还是改前端)
| Console / Shadow 现象 | 改哪里 |
|---|---|
5 ce false 或 wrapper FAIL | 前端 ESM 链(icons / toolbar / rollup) |
5 ce true,hasShadow false,Console 有 Polymer/$ 红字 | 前端:模板、Polymer、360 定制 init() |
5 ce true,hasShadow false,手动 init 可恢复 | 前端:main.ts init 链 + .catch / 时序 |
11 getStreamInfo 空 / 不回调 | C++:AddStream 与当前 Guest frame 未 associate |
11 有 stream,hasShadow true,Shadow 无 #plugin | 前端:initInternal / createPlugin_ 前异常 |
Shadow 有 #plugin,pluginSrc === streamUrl,无 plugin 子 frame | C++:setPdfPluginAttributes、MapToOriginalUrl、PluginResponseWriter |
Shadow 有 #plugin,有子 frame,仍白屏 | C++ connect / PDFium 或 viewport 消息链 |
11.6 与「前三步已确认」的关系
日志里常写「AddStream OK、Guest 在、createPlugin 设计如此」,但 Runtime 第 3 步可能并未完成:
- 没有
hasShadow→ Polymer 未升级 - 没有
originalUrl/ bracket 访问的browserApi→initInternal未跑 - 没有 Shadow 内
#plugin→ 不可能进入 C++ plugin embed
正确顺序: 先 §11.3 确认 Shadow + init → 再 §10 查 L2/L3 C++。
11.7 建议粘贴的 4 项(定责最小集)
排查结束时请保留这 4 项输出:
sameRef/v.ctor/v.connectedv instanceof customElements.get('pdf-viewer')- 打开 PDF 时 第一条红色 error(完整文本)
- 手动 init 后:
after init shadow、plugin是 true 还是 false
据此可区分:main 没 init、Polymer/rollup 升级失败、还是 真进入 C++ plugin 阶段。
9. Checklist 与工具
9.1 升核后必做
C++ / 开关
-
IsOopifPdfEnabled()与产品预期一致(132 对齐 = false) - Guest 页
document.documentElement.hasAttribute('pdfOopifEnabled')符合预期 -
CreateTemplateMimeHandlerPage与 OOPIF 开关一致 -
mimeHandlerPrivate.getStreamInfo或pdfViewerPrivate.getStreamInfo有正常回调
前端
- Guest 页
customElements.get('pdf-viewer')为 true -
document.querySelector('pdf-viewer').shadowRoot存在(§11) - Shadow 内
#scroller/#content/#plugin符合预期 -
chrome.mimeHandlerPrivate.getStreamInfo有streamUrl(§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 #plugin、setPdfPluginAttributes、MapToOriginalUrl |
| Guest Console 诊断 | §11:hasShadow、getStreamInfo(11)、勿用 document 查 #plugin、Release 用 wrapper |
| PluginResponseWriter | 仅 L3 子 frame 导航响应,不是 Guest 的 pdf_viewer.js |
文档类型:团队排查笔记 / 技术博客(脱敏)
适用:Chromium fork、内置 PDF 扩展、定制 pdf360 类前端、132→148 升核
这里写自定义目录标题
- Chromium 148 升核 PDF 白屏:Guest Console 分层诊断与全链路排查实录
- 目录
- 1. 端到端架构:从打开 PDF 到像素上屏
- 2. 132 开关对齐:关闭 OOPIF
- 3. ES Module 到底是什么?算客户端还是前端?
- 4. 「JS 注入」到底指什么?底层机制
- 5. C++ 侧全链路排查
- 6. 前端 ES Module 链排查(本次主阻塞)
- 7. 两种白屏的分叉诊断
- 8. 148 升核变化清单
- 10. Plugin Embed 机制详解(132 非 OOPIF,前三步已通时)
- 11. Guest Console 分层诊断实战
- 9. Checklist 与工具
- 欢迎使用Markdown编辑器
欢迎使用Markdown编辑器
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
新的改变
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
- 全新的界面设计 ,将会带来全新的写作体验;
- 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
- 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
- 全新的 KaTeX数学公式 语法;
- 增加了支持甘特图的mermaid语法1 功能;
- 增加了 多屏幕编辑 Markdown文章功能;
- 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
- 增加了 检查列表 功能。
功能快捷键
撤销: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.
图片:
带尺寸的图片:
居中的图片:
居中并且带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.
// An highlighted block
var foo = 'bar';
生成一个适合你的列表
- 项目
- 项目
- 项目
- 项目
- 项目1
- 项目2
- 项目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)=(n−1)!∀n∈N 是通过欧拉积分
Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=∫0∞tz−1e−tdt.
你可以找到更多关于的信息 LaTeX 数学表达式here.
新的甘特图功能,丰富你的文章
- 关于 甘特图 语法,参考 这儿,
UML图表
可以使用UML图表进行渲染,例如下面产生的一个序列图:
- 关于 UML图表 语法,参考 这儿,
流程图
- 关于 Mermaid 语法,参考 这儿,
FLowchart流程图
我们依旧会支持flowchart.js的流程图语法:
- 关于 Flowchart流程图 语法,参考 这儿.
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。
注脚的解释 ↩︎


2466

被折叠的 条评论
为什么被折叠?



