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

解密Docker代理设置 docker代理设置是指在使用Docker容器时配置代理服务器以允许容器内的应用程序通过代理访问外部资源的过程。代理服务器充当一个中间人,将容器内的网络请求转发到外部网络资源,并将响应返回给容器内的应用程序。在Docker中,代理设置具有重要的作用和重要性,特别是在企业环境和网络限制的情况下。: Docker容器通常需要与外部资源进行通信,例如访问互联网上的API、下载依赖项或执行各种网络操作。代理允许容器内的应用程序通过代理服务器来访问外部资源,这对于企业内部网络或受限制的网络环境尤为重要。 阅读详情

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.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 { ... }

浏览器加载模块时会:

  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.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') → 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 Script在 file:// 等页注入浮动条其它扩展 content_scripts与白屏主因通常无关
DevTools caniuse-lite兼容性数据库里的文件名巧合无完全无关

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

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

  1. Browser 侧 PluginResponseInterceptor(或等价逻辑)识别 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.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 扩展 manifestpdf_extension_util::GetManifest() → IDR_PDF_MANIFEST扩展 ID 固定;定制版 manifest 来自 pdf360 打包
JS/CSS 进 pakBUILD.gn → grit → chrome/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 StreamInfo,SetInternalId
  • 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_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-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_api 走 mimeHandlerPrivate
  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:// 被替换后的 HTML)Browser 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.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含义
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、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 有完整 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 在、无 #plugininitInternal 在 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 + #pluginmain.js 链没走到 initViewer(Promise 失败或时序)
手动 init 仍无 shadowPolymer 模板 / 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 子 frameC++: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 项输出:

  1. sameRef / v.ctor / v.connected
  2. v instanceof customElements.get('pdf-viewer')
  3. 打开 PDF 时 第一条红色 error(完整文本)
  4. 手动 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 升核

这里写自定义目录标题

欢迎使用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)=(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.

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

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. 注脚的解释 ↩︎

Docker 中的代理 docker 中的代理设置分为两类:docker 使用代理访问网络;docker container 使用代理访问网络。因此要注意区分。 阅读详情

相关推荐

Scratch编程必备:5个免费素材下载网站推荐及使用技巧

本文为Scratch编程爱好者提供了构建专属素材库的完整指南。文章推荐了青少儿编程网、小虎鲸Scratch资源站等高质量免费素材来源,并详细介绍了素材的获取、分类管理、优化处理及高效整合到项目中的实用技巧,帮助创作者系统性地解决素材荒问题,提升作品质量与创作效率。

weixin_29230773的博客 1084

Docker 设置代理的三种方法(2025年06月03日亲测可用)

在Docker中设置代理是非常常见的需求,尤其是在某些需要通过代理访问外部资源的网络环境下。本文将介绍三种配置Docker使用代理的常见方法:通过配置镜像加速器、通过配置HTTP/HTTPS代理、以及通过配置代理。

别忘了微笑 8万+

2.4G射频微带线阻抗匹配设计实战(WiFi/BLE)

本文深入探讨了2.4G射频微带线阻抗匹配在WiFi/BLE产品开发中的关键作用。文章详细解析了共面波导微带线结构,提供了基于SI9000工具的阻抗计算实战指南,并总结了不同PCB板厚下的设计参数对照表。同时,文章分享了PCB布局布线、与板厂沟通以及天线匹配调试等核心避坑经验,旨在帮助硬件工程师构建从射频芯片到天线的稳定高效传输通道。

j0k1l2m3n的博客 367

Docker 管理 | 代理配置、内网共享和 Harbor 部署

如何配置容器代理、搭建内网容器注册表以及使用企业级 Harbor 注册表,以提高容器镜像管理的效率和安全性。

RexWang 的博客 7369

磁盘I/O %util特别高

磁盘I/O %util特别高

eagle89的专栏 3086

Xilinx ZYNQ 7000学习笔记二(FSBL代码分析启动-汇编代码)

Xilinx ZYNQ 7000学习笔记

u011116085的博客 4246

docker介绍说明,docker配置代理加速器、docker使用国内镜像仓库

对于面试还是要好好准备的,尤其是有些问题还是很容易挖坑的,例如你为什么离开现在的公司(你当然不应该抱怨现在的公司有哪些不好的地方,更多的应该表明自己想要寻找更好的发展机会,自己的一些现实因素,比如对于我而言是现在应聘的公司离自己的家更近,又或者是自己工作到达了迷茫期,想跳出迷茫期等等)Java面试精选题、架构实战文档你的支持,我的动力;祝各位前程似锦,offer不断!的一些现实因素,比如对于我而言是现在应聘的公司离自己的家更近,又或者是自己工作到达了迷茫期,想跳出迷茫期等等)

m0_60667406的博客 3222

uniapp运行钉钉小程序

因项目原因,公司需要在钉钉里面开发小程序。之前用uniapp开发过app,H5,小程序。还真没尝试过钉钉小程序,今天就简单的记录下uniapp运行钉钉小程序中的过程。

YL 2918

docker安装并配置外网代理

docker简介安装并配置外网代理

miserable_world的博客 7889

windows10家庭版输入loadrunner注册码时报“License security violation. Operation isnot allowed”的解决办法

有可能出现2个错误第一个可能是第二个可能是“License security violation. Operation isnot allowed”ok,在此我在百度找了很多方法,大部分的方法都是删除注册表,但我根本就找不到这个注册表,而后又是管理中心找本地用户和组,但我依旧找不到这个玩意,然后可能是windows10家庭版本来就没有这个。再然后又以管理员身份去运行填写,好的,依旧报错,依旧未解决...

qq_30990097的博客 2558

如何加载交易策略EA到MT4/MT5

1、打开MT4/MT5软件 2、首先,必须有一个EA,而且必须是mq4/ex4为扩展名。点击左上角“文件”—“打开数据文件夹”,找到安装路径 3、将EA的ex4或mq4文件放到MT4目录\MQL4\Experts里面(本文以名为Demo的EA为例) 4、如果有DLL库文件,复制到experts目录下的libraries子目录中 5、如果有tpl模板文件,复制到templates目录中 6、添加完成后重启MT4,点击允许EA运行图标 7、在导航中找到刚才安装的EA,拖拽到...

weixin_41364817的博客 7358

【基础知识】什么是哈希冲突?

哈希冲突(Hash Collision)是指在使用哈希表存储数据时,两个或多个不同的键(Key)被哈希函数映射到同一个位置的情况。因此,在设计哈希表时,应尽量选择合适的哈希函数和哈希表大小,并合理控制装载因子,以减少冲突的发生。因此,在设计哈希表时,应尽量选择合适的哈希函数和哈希表大小,并合理控制装载因子,以减少冲突的发生。是一种解决哈希冲突的方法,它的基本思想是,当发生哈希冲突时,按照某种规则寻找下一个空闲的位置来存储数据。在发生冲突的时候,再用另一个哈希函数算出哈希值,直到算出的哈希值不同为止。

m0_56573171的博客 1万+

NL2SQL实践系列(1):深入解析Prompt工程在text2sql中的应用技巧

NL2SQL实践系列(1):深入解析Prompt工程在text2sql中的应用技巧

丨汀、的博客 4326

Docker网络代理设置

将docker的服务器环境切换到新的网络之后,由于服务器的internet是受限制的(需要连接配置远程代理,不能直接上网)。因此,在使用docker时可以设置网络代理。 # /etc/systemd/system/docker.service.d/http-proxy.conf mkdir -p /etc/systemd/system/docker.service.d cd /etc/systemd/system/docker.service.d cat << EOF ......

海鸥 4341

Kubernetes(K8s)从入门到精通系列之一:一篇文章了解K8s全部核心知识点

Kubernetes(K8s)从入门到精通系列之一:一篇文章了解K8s全部核心知识点

zhengzaifeidelushang的博客 849

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

docker配置代理

创建一个 Docker 配置文件(如果不存在),并在其中添加代理设置。现在您已经成功设置了 Docker 代理。请注意,在某些情况下,您可能还需要为 Docker 容器单独配置代理,具体取决于您的使用场景和网络设置。请注意,将 "your_proxy_server" 和 "proxy_port" 替换为实际的代理服务器和端口号。保存并关闭文件后,重新加载 Docker 守护程序以使更改生效。验证代理设置是否生效。

qq_43615820的博客 7932

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

docker 如何使用代理

其中,如果在使用代理地址时,不希望将运行命令明文写在启动脚本里,可以把用户名、密码存在环境变量,并在执行。时将这两种参数传进去。

duoyasong5907的博客 4554

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

Docker设置网络代理

Docker在通常情况下不会走系统代理,所以我们需要单独给docker进行代理配置。打开docker的服务配置文件:docker.server。保存退出,刷新配置,保险起见重启docker服务。等命令进行测试代理是否成功,此部分不过多赘述。替换为实际的代理服务器地址和端口。命令执行进行首次镜像拉取时会走代理。配置全局的docker网络代理。可以看到docker的代理信息。

BUG编写师的博客 1万+

Docker 使用代理服务器

docker proxy

3219

快速设置 Docker 的三种网络代理配置

目录Docker Client - 代理访问远程的 Docker DaemonDocker Daemon - 代理拉取或推送国外镜像Container - 容器内部代理访问国外资源 通过代理访问网络是一个比较头疼的事情,例如:想访问远程的 Docker Daemon 需要代理,想拉取或推送国外的镜像需要代理,想容器内部访问国外的资源又需要代理。 上面的场景是不是让你很头大,但是请想开点,这篇文章将让你通往自由的国度。保护你的头发,让你留出更多的时间去做该做的事情。 上面说的是三个场景就是针对于不同的网络代理

一只奋斗的猪 7万+

docker 网络代理

Docker代理一共分为3类Docker代理: 控制docker pull等命令的代理container代理: docker run 启动容器的时候,容器里面使用的代理服务器builder代理: docker build时的代理。

99 1257

Docker 配置代理

虽然系统代理设置可以影响一些系统级别的工具和应用,但 Docker 运行在一个隔离的环境中,默认情况下不会继承系统的代理设置。因此,需要显式地为 Docker 配置代理。通过上述步骤配置和验证 Docker 的代理设置,可以确保 Docker 正确通过代理访问网络资源。

2401_85480529的博客 1万+

解决Hash(哈希表)冲突的四种方案

哈希是一种通过对数据进行压缩, 从而提高效率的一种解决方法,但由于哈希函数有限,数据增大等缘故,哈希冲突成为数据有效压缩的一个难题。本文主要介绍哈希冲突、解决方案,以及各种哈希冲突的解决策略上的优缺点。哈希冲突即不同key值产生相同的地址,即发生了hash冲突。一般来说,哈希冲突是无法避免的,所以就有了解决方案。常见的解决Hash冲突的方案有开放寻址法、链地址法和再哈希法。

frozenpenguin的博客 1万+

哈希冲突的详细解释

哈希冲突(Hash Collision)是指在使用哈希表(如HashMapHashtable)进行数据存储时,不同的输入(键)通过哈希函数映射到同一个哈希值(hash value)或者同一个哈希表索引位置的情况。换句话说,即使两个键是不同的,它们的哈希值可能相同,导致它们在哈希表中存储时被分配到同一个存储位置(或桶)中,这就是哈希冲突。

Flying_Fish_roe的博客 3374

OpenClaw Agent 运行时架构深度分析

OpenClaw Agent 运行时架构分析概述OpenClaw 是一个多通道 AI Agent 运行时系统,采用模块化架构设计,支持多种模型提供商、会话类型和工具执行环境。本文档深入分析 Agent 运行时的核心架构、消息处理流程、工具调用机制和 Skills 系统。核心架构系统层次结构┌─────────────────────────────────────────────────────────────┐ │ 通道层 (Channels)

caicongyang 2041

解决哈希冲突(四种方法)

一、了解哈希表及哈希冲突 哈希表:是一种实现关联数组抽象数据类型的数据结构,这种结构可以将关键码映射到给定值。简单来说哈希表(key-value)之间存在一个映射关系,是键值对的关系,一个键对应一个值。 哈希冲突:当两个不同的数经过哈希函数计算后得到了同一个结果,即他们会被映射到哈希表的同一个位置时,即称为发生了哈希冲突。简单来说就是哈希函数算出来的地址被别的元素占用了。 二、解决哈希冲突办法 1、开放定址法:我们在遇到哈希冲突时,去寻找一个新的空闲的哈希地址。 举例:就是当我们去教室上课..

qq_48241564的博客 8万+
上一篇: Windows客户端热修复技术:从原理到工程实践
下一篇: Chromium 下载泡泡架构演变与非标准定制化界面(双环Bug)适配实践
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

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

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

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

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

打赏作者

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

抵扣说明:

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

余额充值