补环境框架:document.all的c++方案(一)

阅读时间:全文 1600 字,预估用时 8 分钟
创作日期:2024-03-14
文章标签:
 
BEGIN

验证和测试请看文章:补环境框架:document.all的c++方案(二)

  一般node中模拟浏览器运行环境都可以在node中用JavaScript代码解决,唯独document.all比较特殊,document.all是无法通过纯JavaScript方案解决的,因此document.all也是很多工程师自己配置补环境框架遇到的一道坎!那么如何解决这个问题呢?

  在github上搜索补环境框架可以看到很多项目,作者看了下它们解决document.all的思路是修改node源码后重新打包上传到项目中,然后通过修改后的node运行补环境框架,说实话,这种被修改过源码的node我只放心在虚拟机上跑。而在sdenv补环境框架用的解决方案是node addon,node addon是node官方提供的插件方案,不需要修改node源码,只需要将c/c++代码转换为node字节码就可以被node引用和使用。

  sdenv项目地址:https://github.com/pysunday/sdenv 🔗

  首先了解下document.all有什么特别之处!

  docuemnt.all返回页面的所有dom节点,功能可以使用document.querySelectorAll(”*“)替代,参考MDN文档:https://developer.mozilla.org/en-US/docs/Web/API/Document/all,而document.all有以下几点特性导致node环境不能模拟: 🔗

  1. document.all的类型值为undefined;
  2. document.all原型指向HTMLAllCollection,因此有类型数组的能力。
  3. document.all是可执行的;

  node addon即node插件(https://nodejs.org/api/addons.html)是node官方提供的能访问node底层和v8底层的方案,我们知道,node其实就是一个封装v8和libuv让JavaScript可以在非浏览器下运行的工具,而node插件又可以直接访问到v8和libuv,因此通过node插件模拟document.all理论上是完全没问题的。 🔗

首先要理清需求,插件需求如下:

  1. 调用这个插件返回一个对象(数组也是对象);
  2. 使用typeof关键词返回类型是undefined,等价于弱等于undefined;
  3. 返回的对象可以当方法调用,调用后返回null。

  第一点需求好解释,只要将该原型定义成对象即可。

  关于第二点需求首先我们找到node对typeof的源码:https://github.com/nodejs/node/blob/main/deps/v8/src/objects/objects.cc#L904C11-L904C11 🔗

Handle<String> Object::TypeOf(Isolate* isolate, Handle<Object> object) {
  if (IsNumber(*object)) return isolate->factory()->number_string();
  if (IsOddball(*object))
    return handle(Oddball::cast(*object)->type_of(), isolate);
  if (IsUndetectable(*object)) {
    // 此处返回undefind
    return isolate->factory()->undefined_string();
  }
  if (IsString(*object)) return isolate->factory()->string_string();
  if (IsSymbol(*object)) return isolate->factory()->symbol_string();
  if (IsBigInt(*object)) return isolate->factory()->bigint_string();
  if (IsCallable(*object)) return isolate->factory()->function_string();
  return isolate->factory()->object_string();
}

  可以看到当目标object是undetectable(IsUndetectable(*object))时才会返回undefined。继续在v8文档(https://v8.github.io/api/head/functions_func_i.html)里找I开头的方法,找到IsUndetectable的接口定义如下: 🔗

◆ IsUndetectable()

bool v8::Object::IsUndetectable () const

True if this object was created from an object template which was marked as undetectable. See v8::ObjectTemplate::MarkAsUndetectable for more information.

https://v8.github.io/api/head/classv8_1_1Object.html#a36d80084eba1eb3a4d43851d217a4a6c 🔗

  翻译接口描述:即如果传入的这个对象是被标记为undetectable则返回true,而标记的方法是v8::ObjectTemplate::MarkAsUndetectable,继续找到MarkAsUndetectable的接口定义如下:

◆ MarkAsUndetectable()

void v8::ObjectTemplate::MarkAsUndetectable ()

Mark object instances of the template as undetectable.

In many ways, undetectable objects behave as though they are not there. They behave like ‘undefined’ in conditionals and when printed. However, 引用字数超出限制.

https://v8.github.io/api/head/classv8_1_1ObjectTemplate.html#a7e40ef313b44c2ad336c73051523b4f8 🔗

  翻译接口描述:即MarkAsUndetectable方法会将对象标记成不可检测,此时该对象类似undefined,但是可以像普通对象一样访问和调用属性。因此只要在node插件里将返回的对象加上undetectable标记就可以模拟typeof返回undefined了。

  上面将对象设置为不可检测的方法是在v8::ObjectTemplate对象模版中实现的,该对象模版用于预设置对象的特性,用于之后实例化。因此继续查看该对象模版的其它api,很容易便能发现在ObjectTemplate的Public Member Functions中MarkAsUndetectable(前面用到过)的上一个api找到SetCallAsFunctionHandler方法,接口定义如下:

◆ SetCallAsFunctionHandler()

void v8::ObjectTemplate::SetCallAsFunctionHandler (FunctionCallback callback, Local data = Local())

Sets the callback to be used when calling instances created from this template as a function. If no callback is set, instances behave like normal JavaScript 引用字数超出限制.

https://v8.github.io/api/head/classv8_1_1ObjectTemplate.html#a1775c8f73e643c339804d2f5b628eddf 🔗

  翻译接口描述:使用SetCallAsFunctionHandler方法为实例设置了回调,实例就可以像函数一样被调用,未设置回调则像普通javascript对象一样不能当方法调用。

  至此我们已经找到两个方法:v8::ObjectTemplate::MarkAsUndetectable(标记对象不可检测)和v8::ObjectTemplate::SetCallAsFunctionHandler(标记对象可以像函数一样被调用),那我们只要在node插件中将对象模版调用这两个方法就可以在用这个对象模版实例化出来的对象拥有对应的特性。

  接下来我们开始编写插件,您也可以直接访问插件源码:https://github.com/pysunday/sdenv/blob/main/bin/documentAll.cc 🔗

  首先定义init方法,init方法的作用是处理exports对象,将我们需要导出的方法放入exports中,这样在NodeJS代码中require这个插件就可以使用我们导出的方法了,代码如下:

void Init(Local<Object> exports, Local<Object> module) {
  Isolate* isolate = exports->GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();
  Local<FunctionTemplate> method_template = FunctionTemplate::New(isolate, GetDocumentAll);
  exports->Set(context, String::NewFromUtf8(isolate, "getDocumentAll").ToLocalChecked(), method_template->GetFunction(context).ToLocalChecked()).FromJust();
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

  上面代码最终导出一个名叫getDocumentAll的方法,且最终由GetDocumentAll接收入参并执行,我们继续定义GetDocumentAll如下:

void MyCallback(const FunctionCallbackInfo<Value>& args) {
  // 直接执行对象返回null
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(Null(isolate));
}

void GetDocumentAll(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();
  Local<ObjectTemplate> obj_template = ObjectTemplate::New(isolate);
  // 增加不可检测特性
  obj_template->MarkAsUndetectable();
  // 增加可调用特性
  obj_template->SetCallAsFunctionHandler(MyCallback);
  Local<Object> obj = obj_template->NewInstance(context).ToLocalChecked();
  if (args.Length() > 0 && args[0]->IsObject()) {
    // 属性拷贝
    Local<Object> argObj = args[0]->ToObject(context).ToLocalChecked();
    Local<Array> propertyNames = argObj->GetPropertyNames(context).ToLocalChecked();
    for (uint32_t i = 0; i < propertyNames->Length(); ++i) {
      Local<Value> key = propertyNames->Get(context, i).ToLocalChecked();
      Local<Value> value = argObj->Get(context, key).ToLocalChecked();
      (void)obj->Set(context, key, value);
    }
  }
  args.GetReturnValue().Set(obj);
}

  该方法创建一个名叫obj_template的对象模板,并将该对象模版设置为不可检测特性(obj_template->MarkAsUndetectable())和可被调用特性(obj_template->SetCallAsFunctionHandler(MyCallback)),然后将obj_template实例化出一个叫obj的模板对象,接着将接收的入参对象属性拷贝到obj最后返回给调用方。

  最后给代码添加上命名空间并引用v8命名空间内用到的名称,最终代码如下:

#include <node.h>
namespace documentAll {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::String;
using v8::Value;
using v8::Null;
using v8::Array;

void MyFunctionCallback(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(Null(isolate));
}

void GetDocumentAll(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();
  Local<ObjectTemplate> obj_template = ObjectTemplate::New(isolate);
  obj_template->MarkAsUndetectable();
  obj_template->SetCallAsFunctionHandler(MyFunctionCallback);
  Local<Object> obj = obj_template->NewInstance(context).ToLocalChecked();
  if (args.Length() > 0 && args[0]->IsObject()) {
    Local<Object> argObj = args[0]->ToObject(context).ToLocalChecked();
    Local<Array> propertyNames = argObj->GetPropertyNames(context).ToLocalChecked();
    for (uint32_t i = 0; i < propertyNames->Length(); ++i) {
      Local<Value> key = propertyNames->Get(context, i).ToLocalChecked();
      Local<Value> value = argObj->Get(context, key).ToLocalChecked();
      (void)obj->Set(context, key, value);
    }
  }
  args.GetReturnValue().Set(obj);
}

void Init(Local<Object> exports, Local<Object> module) {
  Isolate* isolate = exports->GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();
  Local<FunctionTemplate> method_template = FunctionTemplate::New(isolate, GetDocumentAll);
  exports->Set(context, String::NewFromUtf8(isolate, "getDocumentAll").ToLocalChecked(), method_template->GetFunction(context).ToLocalChecked()).FromJust();
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
}
FINISH

随机文章
人生倒计时
default