【node源码-8】node源码接入jsdom
完成了jsdom 的底层自动使用。遇到自定义函数比如navigator_userActivation_get 等jsdom缺少的,可以自己定义。
先看一下结果:
代码如下:
const vm = require('vm');const { JSDOM } = require('jsdom');const html = `<!DOCTYPE html><html><head><title>My Custom Page</title></head><body><div id="app">Hello World</div></body></html>`;const dom = new JSDOM(html, {url: 'http://localhost:3000',pretendToBeVisual: true,resources: 'usable'});// 2. 配置 KhBox 并注册 jsdomKhBox.setTraceLog(true); // 开启日志KhBox.setJsdomWindow(dom.window);// jsdom对象传到c层// 3. 注册自定义的 envFuncs(jsdom 不支持的 API)KhBox.envFuncs = {// navigator.userActivation(jsdom 不支持)- getternavigator_userActivation_get: function() {console.log('[CUSTOM] navigator_userActivation_get called');return {hasBeenActive: true,isActive: false};},};console.log('[MAIN] jsdom and envFuncs registered');const sandbox = vm.createContext({console: console // 普通的vm用法});// 4. 在 vm 沙箱里执行代码const code = `console.log('[VM] === 基本测试 ===');console.log('[VM] document.title:', document.title);document.title = 'Modified in VM';console.log('[VM] new title:', document.title);console.log('[VM] navigator.userAgent:', navigator.userAgent);console.log('[VM] location.href:', location.href);document.cookie = 'test=123';console.log('[VM] document.cookie:', document.cookie);console.log('[VM] window.innerWidth:', window.innerWidth);try {localStorage.setItem('foo', 'bar');console.log('[VM] localStorage.getItem:', localStorage.getItem('foo'));} catch(e) {console.log('[VM] localStorage error:', e.message);}console.log('[VM] === 测试完成 ===');`;const code3=`var a={"age":33};console.log(a.age)`try {vm.runInContext(code, sandbox);vm.runInContext(code3, sandbox);console.log('[MAIN] VM execution successful');} catch(err) {console.error('[MAIN] VM execution failed:', err.message);console.error(err.stack);}console.log('[MAIN] Demo finished');
日志如下:
D:\Code\C\node\out\Release>node.exe demo.js[KhBox] Trace logging enabled[KhBox] jsdom window set in Environment[KhBox] jsdom window registered via setJsdomWindow()[MAIN] jsdom and envFuncs registered[VM] === 基本测试 ===[KhBox] Found in jsdom: document[KhBox] {vm-jsdom|get} caller:[Object] -> prop:[document] -> result:[object: Document][KhBox] Created ProxyObject for: document[KhBox] {proxy|get} caller:[Object] -> prop:[title] -> result:[string: "My Custom Page"][VM] document.title: My Custom Page[KhBox] Found in jsdom: document[KhBox] {vm-jsdom|get} caller:[Object] -> prop:[document] -> result:[object: Document][KhBox] Created ProxyObject for: document[KhBox] {proxy|set} caller:[Object] -> prop:[title] -> value:[string: "Modified in VM"][KhBox] Found in jsdom: document[KhBox] {vm-jsdom|get} caller:[Object] -> prop:[document] -> result:[object: Document][KhBox] Created ProxyObject for: document[KhBox] {proxy|get} caller:[Object] -> prop:[title] -> result:[string: "My Custom Page"][VM] new title: My Custom Page[KhBox] Found in jsdom: navigator[KhBox] {vm-jsdom|get} caller:[Object] -> prop:[navigator] -> result:[object: Navigator][KhBox] Created ProxyObject for: navigator[KhBox] {proxy|get} caller:[Object] -> prop:[userAgent] -> result:[string: "Mozilla/5.0 (win32) AppleWebKit/537.36 (KHTML, lik..."][VM] navigator.userAgent: Mozilla/5.0 (win32) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/27.4.0[KhBox] Found in jsdom: location[KhBox] {vm-jsdom|get} caller:[Object] -> prop:[location] -> result:[object: Location][KhBox] Created ProxyObject for: location[KhBox] {proxy|get} caller:[Object] -> prop:[href] -> result:[string: "http://localhost:3000/"][VM] location.href: http://localhost:3000/[KhBox] Found in jsdom: document[KhBox] {vm-jsdom|get} caller:[Object] -> prop:[document] -> result:[object: Document][KhBox] Created ProxyObject for: document[KhBox] {proxy|set} caller:[Object] -> prop:[cookie] -> value:[string: "test=123"][KhBox] Found in jsdom: document[KhBox] {vm-jsdom|get} caller:[Object] -> prop:[document] -> result:[object: Document][KhBox] Created ProxyObject for: document[KhBox] {proxy|get} caller:[Object] -> prop:[cookie] -> result:[string: ""][VM] document.cookie:[KhBox] Found in jsdom: window[KhBox] {vm-jsdom|get} caller:[Object] -> prop:[window] -> result:[object: Window][KhBox] Created ProxyObject for: window[KhBox] {proxy|get} caller:[Object] -> prop:[innerWidth] -> result:[number: 1024.000000][VM] window.innerWidth: 1024[KhBox] Found in jsdom: localStorage[KhBox] {vm-jsdom|get} caller:[Object] -> prop:[localStorage] -> result:[object: Object][KhBox] Created ProxyObject for: localStorage[KhBox] {proxy|get} caller:[Object] -> prop:[setItem] -> result:[function: setItem][KhBox] localStorage->has (null)
成功了一点,运行
KhBox.setJsdomWindow(dom.window);
后,c层会自动去调用jsdom的方法,显示get ,set 。不过日志有点杂乱。
next:
-
自己实现的方法KhBox.envFuncs 应该优先于jsdom。如果不写 就是默认jsdom,或者抛异常方法不存在。
-
让自己写的方法生效。
-
目前只在PropertyGetterCallback内部进行了一点修改,其他的set等 应该也得修改,比如自己没有处理过的proxyobj,被直接set ,就需要在PropertySetterCallback 中进行输出。
接下来看一下大概思路。
一、整体流程总览
-
JS:demo.js 创建 jsdom 和 KhBox,注册 dom.window、配置khBox.envFuncs -
JS:在 vm 沙箱里执行脚本,访问 document / navigator / ... -
C++: node_contextify.cc里的全局属性拦截(PropertyGetterCallback)接管访问 -
先看沙箱本地对象 -
再走 koohai: GetFromJsdom/GetFromKhBox -
C++:如果是 jsdom 对象(比如 document),用CreateProxyObject包一层带拦截器的 ProxyObject -
JS:在 khBox.envFuncs里定义的函数实际执行业务逻辑,或者让 jsdom 完成默认行为
二、JS 侧:jsdom 与 KhBox 的准备工作
在 demo.js 中,做了三件核心事情:
-
创建 jsdom:
-
new JSDOM(html, { url, pretendToBeVisual, resources })
-
把 jsdom 的 window注册给 C++:
-
使用全局 KhBox单例:khBox.setJsdomWindow(dom.window)
-
给 khBox.envFuncs挂自己的函数:
-
比如: -
navigator_userActivation_get -
location_href_get / location_href_set -
document_cookie_get / document_cookie_set -
localStorage_setItem / localStorage_getItem -
Node_appendChild
这些函数是后面 C++ 通过 GetFromKhBox 和 CallKhBoxFunction 找到并执行的核心。
三、node_contextify.cc:VM 全局属性拦截与三级查找
VM 沙箱里的 document / navigator 首次访问时,会触发 node::contextify::ContextifyContext::PropertyGetterCallback(在这里做了增强)。
这一步做的是“全局属性三级查找”:
-
沙箱本地对象(sandbox 自身) -
global_proxy(Node 原有的 global 对象代理) -
koohai 扩展: -
先 GetFromJsdom(env, context, name, &result) -
再 GetFromKhBox(env, context, name, &result)
同时,如果从 jsdom 拿到的是对象(如 window/document/navigator),这里会调用:
-
CreateProxyObject(env, result, name)
直接把 jsdom 原始对象包成一个带完整 trap 的 ProxyObject,以便后面深层访问也能被拦截。
四、node_koohai_interceptor.cc:jsdom 与 KhBox.envFuncs 的查找
1. GetFromJsdom:从 jsdom 取原始对象
大致流程:
-
通过 Environment拿出之前设置的jsdom window: -
在 KhBox::SetJsdomWindow里,把dom.window存到了env上 -
根据 property 名字决定到底返回 window 上哪个对象: -
比如 document→window.document -
navigator→window.navigator -
location→window.location
在日志中,能看到类似:
-
[KhBox] Found in jsdom: document
这说明 GetFromJsdom 找到了对应对象。
2. GetFromKhBox:从 khBox.envFuncs 取补丁函数
还未生效
五、node_koohai_proxy.cc:ProxyObject 深层拦截与命名规则
当 node_contextify.cc 从 jsdom 拿到 document 这类对象时,不是直接返回,而是调用:
-
CreateProxyObject(env, target, "document")
CreateProxyObject 负责:
-
创建一个 ObjectTemplate,设置NamedPropertyHandlerConfiguration: -
get: ProxyPropertyGetter -
set: ProxyPropertySetter -
has / deleteProperty / ownKeys / defineProperty / getOwnPropertyDescriptor 等 -
实例化对象 proxy -
把“真实目标对象”和“基名”塞进 internal fields: -
kTargetObject:指向 jsdom 的原始对象 -
kPropertyName:比如"document"、"navigator"
后续对 proxy.xxx 的操作,就都会走到这些 C++ 回调里。
1. Getter:ProxyPropertyGetter
逻辑简化为:
-
从 internal fields 读出: -
base_name:比如"navigator" -
property:比如"userActivation" -
拼出 envFuncs 查找 key: -
lookup_key = base_name + "_" + property + "_get" -
如: "navigator_userActivation_get"、"document_cookie_get"、"location_href_get" -
调 CallKhBoxFunction(env, lookup_key, ...) -
记录日志:谁 get 了谁,结果是什么 -
把结果返回给 JS -
如果 khBox.envFuncs[lookup_key]存在并返回值,则: -
否则回退到 jsdom 原始对象: target_obj->Get(context, property)
2. Setter:ProxyPropertySetter
逻辑类似:
-
一样拿 base_name和property -
拼 key:
例子:
- `lookup_key = base_name + "_" + property + "_set"`- `document.cookie = ...` → `"document_cookie_set"`- `location.href = ...` → `"location_href_set"`
-
记录 set 日志(谁设置了谁,设置了什么) -
CallKhBoxFunction(env, lookup_key, 1, argv, ...) -
在 khBox.envFuncs里控制最终行为
其他的has 等以及一些属性定义的后续完善。
-
夜雨聆风
