深水区解析:window.external 的前世今生以及核心机制

—— 从 IE ActiveX 到 Chromium Renderer 注入的非标准通信机制

关键词:window.external、Chromium、V8 注入、Renderer、IPC、非标准 API、浏览器内核
适合人群:浏览器内核工程师 / 客户端工程师 / Hybrid 架构开发者


在 Chromium 体系下谈 JS 与 C++ 通信,如果只讲 postMessage、Extension、Mojo,那其实永远绕不开一个“历史包袱级别”的机制

window.external

它不是 Web 标准
不属于 Blink API
甚至在官方文档中几乎“被忽略”

但在 国内浏览器 / 企业定制 / 老业务系统 中,它却真实存在了 十几年,并且至今仍在大量代码中“活着”。

这篇文章,我们不从“怎么用”讲起,而是:

站在浏览器内核工程师的视角,完整拆解 window.external 的设计动机、实现路径、架构定位、安全问题,以及它为什么注定被淘汰。


一、window.external 的历史背景与设计动机

1️⃣ window.external 从哪里来?

window.external 并不是 Chromium 发明的。

它的源头可以追溯到 IE / Trident 内核时代,本质上是 ActiveX / COM 的 JavaScript 暴露形式

在 IE 中,宿主程序(如 Outlook、Office、企业客户端)可以:

  • 向网页注入 COM 对象

  • JS 通过 window.external.xxx() 直接调用本地能力

经典 IE 时代代码示例:

window.external.AddFavorite(url, title); window.external.RunShellCommand("calc.exe"); 

这在当年是完全合法、甚至被鼓励的做法

设计初衷非常明确:

让网页成为“壳”,本地程序才是“核心”。


2️⃣ 为什么 Chromium 也实现了 window.external?

如果你从“Web 标准洁癖”的角度看 Chromium,会觉得这很奇怪:

Chromium 一向强调安全、沙箱、最小权限
那为什么还要保留这种“野蛮接口”?

答案只有一句话:

历史 + 现实 + 商业

在国内浏览器生态中:

  • 大量 老业务系统

  • 政企内部系统

  • PC 客户端 + Web 混合架构

严重依赖以下模式:

window.external.invoke(...) window.external.CallNative(...) 

这些系统往往:

  • 不可能重写

  • 不可能升级成 Extension

  • 不可能接受严格权限模型

所以对很多厂商来说:

window.external 是“兼容成本最低”的宿主通信方案


二、window.external 在 Chromium 架构中的真实定位

1️⃣ 它不属于 Web API

这是理解 window.external 的第一道门槛

不在

  • Blink Web IDL

  • DOM API

  • 标准 JS Runtime

它属于一个完全不同的类别:

宿主注入(Host Object Injection)

也就是说:

这是 C++ 主动向 V8 Context 注入的“外挂对象”


2️⃣ 进程层级关系(极其关键)

window.external 的存在位置,决定了它的能力边界。


关键结论:

  • window.external 一定存在于 Renderer

  • 真正的能力永远在 Browser / Native

window.external 本身不是能力提供者,而是:

JS → C++ → IPC → Native 的“入口点”


三、window.external 的总体实现模型

一句话抽象模型:

在 Renderer 中注册一个 V8 Object → JS 调用 → C++ 捕获 → IPC → Browser 执行

我们可以拆解为 6 个阶段

  1. Renderer 创建 C++ 扩展对象

  2. 注入到 window

  3. 绑定 JS 方法

  4. JS 调用进入 V8 回调

  5. 参数序列化

  6. IPC 发往 Browser

这是一个非常典型的“非标准通信路径”


四、关键实现路径(Chromium 源码级)

下面这部分,是内核工程师真正关心的地方

1️⃣ 注入时机:DidClearWindowObject

window.external 的注入不是随便找个地方做的。

唯一正确、稳定的注入时机是:

void ChromeRenderFrameObserver::DidClearWindowObject() { InstallExternalObject(render_frame()); } 

DidClearWindowObject 的含义是:

  • 新的 JS Context 被创建

  • 页面导航 / 刷新 / iframe 初始化完成

这是 V8 Context 生命周期中唯一安全的注入点


2️⃣ 创建 V8 ObjectTemplate

v8::Local<v8::ObjectTemplate> external = v8::ObjectTemplate::New(isolate); external->Set( v8::String::NewFromUtf8Literal(isolate, "invoke"), v8::FunctionTemplate::New(isolate, InvokeCallback)); 

这里发生了三件非常重要的事:

行为说明
ObjectTemplateJS 对象“蓝图”
FunctionTemplateJS ↔ C++ 绑定
isolate绑定当前 V8 实例

3️⃣ 挂载到 window.external

v8::Local<v8::Object> external_obj = external->NewInstance(context).ToLocalChecked(); context->Global()->Set( context, v8::String::NewFromUtf8Literal(isolate, "external"), external_obj); 

这一行执行完之后:

window.external // 出现在 JS 世界中 

从此开始,JS 与宿主之间的“禁忌之门”被打开。


五、JS → C++ 调用链深度拆解

1️⃣ JS 侧调用方式

window.external.invoke( "ReadFile", JSON.stringify({ path: "c:\\test.txt" }) ); 

注意这里的 JSON.stringify,这是一个非常典型的坑。


2️⃣ 进入 V8 FunctionCallback

void InvokeCallback( const v8::FunctionCallbackInfo<v8::Value>& args) { v8::Isolate* isolate = args.GetIsolate(); } 

此时你已经处于:

Renderer 进程 + V8 调用栈


3️⃣ 参数解析(工程实践中的坑王)

std::string method; std::string json; if (args.Length() >= 2) { method = V8ToString(args[0]); json = V8ToString(args[1]); } 

⚠️ 常见错误:

window.external.invoke("ReadFile", { path: "c:\\a.txt" }); 

C++ 中你拿到的将是:

[object Object] 

所以JSON 是事实上的“协议层”


4️⃣ IPC 发送

Send(new ExtensionHostMsg_ExternalCall( routing_id(), method, json )); 

从这一刻起:

  • Renderer 不再持有能力

  • 所有风险、权限、系统操作都在 Browser


六、Browser 侧处理模型

1️⃣ IPC 接收

bool BrowserMessageFilter::OnMessageReceived( const IPC::Message& message) { IPC_MESSAGE_HANDLER(ExternalCall, OnExternalCall) } 

2️⃣ Native 分发

void OnExternalCall( const std::string& method, const std::string& json) { if (method == "ReadFile") { HandleReadFile(json); } } 

3️⃣ 执行系统能力

这一层已经完全脱离 Web 世界:

  • 文件系统

  • 注册表

  • 登录态

  • 本地设备

  • 系统服务

这是一个彻底绕过 Web 安全模型的执行环境


七、返回值与回调模型设计

1️⃣ 同步返回(不推荐)

args.GetReturnValue().Set( v8::String::NewFromUtf8(isolate, result.c_str()) ); 

❌ 问题非常明显:

  • 阻塞 Renderer

  • 无法跨进程等待

  • 极易造成卡顿


2️⃣ 异步回调(主流方案)

JS:

window.external.invoke("ReadFile", json, function(result) { console.log(result); }); 

C++:

  • 保存 callback id

  • Browser 执行完成

  • 反向 IPC

  • Renderer 执行 JS 回调


八、安全与设计缺陷(它为什么必然被淘汰)

1️⃣ 完全绕过 Web 安全模型

机制是否生效
Same-Origin
CSP
Permissions

只要 JS 能执行:

window.external.invoke("DeleteFile", "C:\\Windows"); 

2️⃣ 无 Schema、无约束

  • 参数是字符串

  • 无类型系统

  • 无接口描述

  • 无权限分级


3️⃣ 生命周期极其混乱

  • iframe reload

  • 页面 crash

  • Native 仍在执行

最终结果:

callback 丢失,资源泄漏,难以维护


九、window.external 的现实定位

维度评价
易用性⭐⭐⭐⭐⭐
安全性
可维护性
现代化
历史兼容⭐⭐⭐⭐⭐

十、工程师总结一句话

window.external 是一个“把浏览器当 ActiveX 用”的时代产物。

它简单、直接、暴力
能快速解决问题
但注定无法支撑:

  • Chromium 的安全模型

  • 多进程架构

  • 长期可维护性

它不是“错”,只是不属于这个时代

十一、JS 是如何运行在 V8 里的?

—— 理解 window.external / JS ↔ C++ 通信之前必须补上的“底层真相”

在解释 window.external、Mojo、Extension、IPC 之前,有一个问题如果没有想清楚,后面的所有机制都会显得“像魔法”:

JavaScript 明明是一门脚本语言,它到底是怎么运行起来的?
又为什么能直接调用 C++?

这一章,我们完全不从 API 讲,而是从 V8 与 Chromium 内核视角,把 JS 的“运行本质”彻底拆开


11.1 一个必须先接受的结论

先给出一个很多前端从未被明确告诉过的事实

JavaScript 并不是自己在运行,而是 V8 这个 C++ 程序在“执行 JS 代码”。

也就是说:

  • JS 不是操作系统调度的进程

  • 不是虚拟机里独立跑的程序

  • 而是 C++ 主动加载、解析、编译并执行的一段文本

这一点,是理解 JS ↔ C++ 一切通信机制的起点


11.2 V8 在 Chromium 中的真实角色

从工程角度看,V8 的定位非常清晰:

  • 它是一个 用 C++ 编写的 JavaScript 引擎

  • 提供:

    • JS 语法解析器(Parser)

    • 解释器(Ignition)

    • JIT 编译器(TurboFan)

    • 垃圾回收器(GC)

    • 对象与原型系统

在 Chromium 的 Renderer 进程中,真实结构是:

Chromium Renderer(C++) └── Blink(DOM / Web API) └── V8(执行 JS) 

JS 永远寄生在宿主程序中运行,它不能脱离宿主独立存在。


11.3 在 V8 眼中,JS 代码是什么?

当你写下这样一段 JS:

console.log("hello"); 

在 V8 看来,它的本质只是:

"console.log(\"hello\")" 

也就是说:

JS 源码在进入 V8 之前,只是一段字符串


11.4 JS 是如何“被运行”的?

在 Chromium Renderer 中,JS 的执行流程(高度抽象后)如下:

// 1. 创建 V8 Isolate(一个 JS 虚拟机实例) v8::Isolate* isolate = v8::Isolate::New(...); // 2. 创建 Context(一个 JS 世界) v8::Local<v8::Context> context = v8::Context::New(isolate); // 3. 进入这个 JS 世界 v8::Context::Scope context_scope(context); // 4. 编译 JS 源码 v8::Local<v8::Script> script = v8::Script::Compile(context, source).ToLocalChecked(); // 5. 执行 JS script->Run(context); 

这就是 JS“运行”的全部秘密。

没有线程魔法
没有语言特权

👉 JS 的每一行代码,都是 C++ 主动调用 Run() 执行的


11.5 V8 Context:JS 的“世界”是什么?

v8::Context 可以理解为:

一个完整的 JS 世界(Realm)

它包含:

  • 全局对象(window / global)

  • 变量作用域

  • 原型链

  • 内建对象(Array / Object / Function)

几个关键事实:

  • 页面刷新 → Context 销毁

  • iframe → 新 Context

  • 同一个页面可以有多个 Context


11.6 window 是谁创建的?

这是一个非常颠覆直觉但极其重要的事实

window 不是 JS 创建的。

真实流程是:

C++ 创建 window 对象 ↓ 挂载到 V8 Context.global ↓ JS 才能访问 window 

JS 只能使用宿主提供的对象,而不能反向定义它们。

这也是为什么:

  • JS 无法创建 document

  • JS 无法创建 location

  • JS 无法创建 window.external


11.7 JS 调用函数时,V8 内部发生了什么?

当 JS 执行:

foo(1, 2); 

V8 会经历三个阶段:

1️⃣ 解析源码,生成 AST
2️⃣ 生成字节码(Ignition)
3️⃣ 执行字节码 / JIT 编译后执行


11.8 如果 foo 是 JS 函数

function foo(a, b) { return a + b; } 
  • V8 执行 JS 字节码

  • 完全在 JS 引擎内部完成


11.9 如果 foo 是 C++ 函数(关键转折点)

如果 C++ 这样注册了一个函数:

v8::FunctionTemplate::New(isolate, FooCallback); 

并把它挂到 JS 世界:

global->Set(context, "foo", foo_function); 

JS 调用:

foo(1, 2); 

此时真实调用链是:

JS 调用 foo ↓ V8 发现这是 Host Function ↓ 直接调用 FooCallback(C++) 

👉 JS 并没有“跨语言调用”,只是 V8 在执行 C++ 回调。


11.10 JS 为什么能“感知” C++ 的存在?

因为 V8 提供了两种核心能力:

1️⃣ Host Object
C++ 可以创建一个“像 JS 一样”的对象

2️⃣ Host Function
C++ 可以把一个函数挂进 JS 世界

JS 根本不知道背后是 C++,它只看到:

window.external.invoke(...) 

11.11 JS 以为自己是“独立运行”的原因

这是浏览器刻意营造的“抽象幻觉”。

JS 看到的是:

JS API背后真实实现
windowC++
documentBlink
fetchNetwork Service
setTimeoutBrowser Timer
consoleDevTools

👉 JS 只是“接口层”,能力永远在 C++。


11.12 回到 window.external:一切突然都合理了

现在再回看 window.external:

  • 它不是 JS 特权

  • 不是魔法

  • 只是:

    • C++ 创建对象

    • 注入到 V8 Context

    • JS 调用触发 C++ 回调

    • 再通过 IPC 调到 Browser

window.external 的“危险”,不是它能通信,而是它通信得太直接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

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

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

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

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

打赏作者

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

抵扣说明:

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

余额充值