【node阅读-1】node架构了解

上篇还是唐突了,上来直接打田英,被秒的毫无还手之力。于是先搜了一下整体攻略。

一、node整体架构

Node.js 代码主要是分为三个部分,分别是C、C++ 和 JS。

  1. JS 代码就是我们平时在使用的那些 JS 的模块,比方说像 http 和 fs 这些模块。
  2. C++ 代码主要分为三个部分,第一部分主要是封装 Libuv 和第三方库的 C++ 代码,比如net 和 fs 这些模块都会对应一个 C++ 模块,它主要是对底层的一些封装。第二部分是不依赖 Libuv 和第三方库的 C++ 代码,比方像 Buffer 模块的实现。第三部分 C++ 代码是 V8 本身的代码。
  3. C 语言代码主要是包括 Libuv 和第三方库的代码,它们都是纯 C 语言实现的代码。

1.Libuv

是一个异步模块 ,c实现的 ,先pass

临时了解一点就是, 阻塞任务是放线程池里的,不阻塞主线程

当应用层提交任务时,比方说像 CPU 计算还有文件操作,这种时候不是交给主线程去处理的,而是直接交给线程池处理的。线程池处理完之后它会通知主线程。

2.v8

V8 在 Node.js 里面主要是有两个作用,第一个是负责解析和执行 JS。第二个是支持拓展 JS 能力,作为这个 JS 和 C++ 的桥梁

Isolate:首先第一个是 Isolate 它是代表一个 V8 的实例,它相当于这一个容器。通常一个线程里面会有一个这样的实例。比如说在 Node.js主线程里面,它就会有一个 Isolate 实例。

Node.js 的主线程就是一个 Isolate,都是对立的 ,

Context:Context 是代表我们执行代码的一个上下文,它主要是保存像 Object,Function 这些我们平时经常会用到的内置的类型。如果我们想拓展 JS 功能,就可以通过这个对象实现。

比如chrome两个标签页,通用一个chrome但是互不通上下文

ObjectTemplate:ObjectTemplate 是用于定义对象的模板,然后我们就可以基于这个模板去创建对象。

OFunctionTemplate:FunctionTemplate 和 ObjectTemplate 是类似的,它主要是用于定义一个函数的模板,然后就可以基于这个函数模板去创建一个函数。

FunctionCallbackInfo: 用于实现 JS 和 C++ 通信的对象。

当 JS 调用 C++ 函数时,V8 会把 JS 传过来的参数(Args)、this 指针、返回值的位置,全部打包成一个 FunctionCallbackInfo 对象传给 C++

Handle:Handle 是用管理在 V8 堆里面那些对象,因为像我们平时定义的对象和数组,它是存在 V8 堆内存里面的。Handle 就是用于管理这些对象。****V8 的 Handle 是为了应对对象移动

HandleScope:HandleScope 是一个 Handle 容器,HandleScope 里面可以定义很多 Handle**,它主要是利用自己的生命周期管理多个 Handle。**

这个demo 就是新增了一个自定义的函数,demo.test () ,那我们的document.all呢

先回忆以下document.all:

明明存在,但 typeof 是 'undefined',做 if 判断时是 false,且 == null 为 true),在 V8 的术语中被称为 Undetectable(不可检测的对象)。

plain 复制代码
v8::Isolate* isolate = v8::Isolate::New(create_params);
v8::Isolate::Scope isolate_scope(isolate);
v8::HandleScope handle_scope(isolate);

//上下文
v8::Local<v8::Context> context = v8::Context::New(isolate);
v8::Local<v8::ObjectTemplate> ghost_template =v8::ObjectTemplate::new(isolate);

ghost_template->MarkAsUndetectable();
ghost_template->Set(isolate,"name",v8::String::NewFormUf8Literal(isolate,"im a goase");

// 实例化对象 编译 
v8::Local<v8::Object> ghost_instance=ghost_template->NewINstance(context).ToLocalChecked();

//设置全局变量
v8::Local<Object> globalInstance = context->Global();
globalInstance->Set(context, v8::String::NewFromUtf8Literal(isolate, "myGhost"),  ghost_instance
).Check();
//js里能直接找到myGhost 对象。  

3.js和c++的通信

当在 JS 层调用刚才定义 test 函数时,就会相应的执行 C++ 层的 test 函数。这个函数有一个入参是 FunctionCallbackInfo,在 C++ 中可以通过这个对象拿到 JS 传来一些参数,这样就完成了 JS 层到 C++ 层通信。经过一系列处理之后,还是可以通过这个对象给 JS 层设置需要返回给 JS 的内容,这样可以完成了 C++ 层到 JS 层的通信。

4.加载js的流程

Node.js 中有五种模块加载器。

  1. JSON 模块加载器
  • 不做任何逻辑执行,只是单纯的数据读取。
  1. 用户 JS 模块加载器
plain 复制代码
// Node.js 偷偷加的头
(function (exports, require, module, __filename, __dirname) { 
    
    // --- 这里面才是你写的代码 ---
    const a = 1;
    module.exports = a;
    // -------------------------

// Node.js 偷偷加的尾
});
  1. 原生 JS 模块加载器
    1. Node.js 自带的模块(如 http, fs)。读内存 -> 包装执行 -> 传入特殊的 internalBinding 用来调 C++。
  2. 内置 C++ 模块加载器
    1. Node.js 初始化时注册链表 -> 根据名字找节点 -> 执行钩子。
  3. Addon 模块加载器

直击核心,怎么把jsdom 内置到node ,方便后面补环境直接内置,2. 怎么让js调用走到我们自己的c++函数里,要hook哪个函数吗还是有拦截器 ,ai答案如下:

第一部分:怎么把 jsdom 内置到 Node.js 中?

首先你要认清一个事实:jsdom** ****本质上是一个巨大的、纯 JavaScript 写的库**,它并不是 C++ 模块。它依赖了 parse5, tough-cookie, cssstyle 等几十个其他 npm 包。

如果你想让 Node.js 一启动就自带 jsdom(不需要 npm install),通常有两条路:

方案 A:使用 Node.js 的快照功能(Snapshot)------ 推荐

这是 Node.js 18+ 提供的现代方案。你可以把 jsdom 加载到内存堆中,然后把这个堆"保存"下来。下次启动 Node.js 时,直接恢复这个堆,jsdom 就已经在里面了。

步骤逻辑:

  • 写一个脚本 snapshot.js,里面 require('jsdom') 并挂载到 global。
  • 编译 Node.js 时使用 --build-snapshot 参数或者运行时生成快照。
  • 分发这个带有快照数据的 Node.js 二进制文件。

这种方式不需要修改 Node.js 源码,只是利用了 V8 的快照机制。

方案 B:硬改 Node.js 源码(把 JS 打包进二进制)------ 极难

如果你非要像 http 或 fs 模块一样把 jsdom 内置进 Node.js 源码:

  • 移动文件 :你需要把 jsdom 及其所有依赖包(这非常多)的源码放到 Node.js 源码的 lib/ 目录下(或者 deps/ 目录)。
  • 修改构建脚本:修改 node.gyp,把这些新加入的 JS 文件列入编译列表。Node.js 在编译时会把 lib/ 下的 JS 文件转成 C++ 里的字符数组(这就是"内置 JS 模块"的原理)。
  • 暴露模块:你需要修改 lib/internal/bootstrap/node.js 或者直接在你的 JS 里使用 internalBinding 来让外部能访问到它。
第二部分:怎么执行自己的函数

就是刚才的 document.all ,做一个函数模板 ,挂载全局

第三部分:是否有拦截器

DOM 有个特性:你访问 window.abc,即使 abc 没定义,浏览器内部也可能去查找 ID 为 abc 的元素。或者 document.body 这种属性,它不是简单的对象属性,而是对应到底层的 Getter。

这就需要用到 V8 的 Accessors (访问器)Interceptors (拦截器/属性处理器)

场景: 只要 JS 访问 obj.xxx,无论 xxx 是什么,都先走到 C++ 函数里。

plain 复制代码
// 这是一个"属性拦截器"回调
// property: JS 访问的属性名 (比如 "body", "div1")
// info: 其他信息
void MyPropertyGetter(v8::Local<v8::Name> property,
                      const v8::PropertyCallbackInfo<v8::Value>& info) {
    v8::Isolate* isolate = info.GetIsolate();
    v8::String::Utf8Value key(isolate, property);
    
    printf("JS 正在尝试访问属性: %s\n", *key);

    // 可以在这里判断,如果是 "body",就返回一个 C++ 包装的 Body 对象
    if (strcmp(*key, "body") == 0) {
        info.GetReturnValue().Set(v8::String::NewFromUtf8Literal(isolate, "[Body Element]"));
    }
    // 如果不设置返回值,V8 会继续按正常流程查找属性
}

// 注册拦截器
void SetupGhostObject(v8::Isolate* isolate, v8::Local<v8::ObjectTemplate> templ) {
    // SetHandler 用于设置拦截器
    // NamedPropertyHandlerConfiguration 专门拦截字符串类型的属性访问
    templ->SetHandler(v8::NamedPropertyHandlerConfiguration(MyPropertyGetter));
}

这个拦截器 就相当于proxy了,找函数追踪。

后面再找找怎么保护函数 应该就可以了。

二、kale

什么是宏,和函数的区别,哪里用到宏

核心区别对比表

特性 宏 (#define) 函数 (Function)
处理阶段 预处理阶段 (编译之前) 编译/运行阶段
本质 文本替换 (Copy-Paste) 代码跳转 (Call/Return)
代码体积 变大 (用了多少次就粘贴多少份代码) 不变 (逻辑只有一份,重复调用)
运行速度 极快 (没有调用开销,直接执行指令) 有微小开销 (压栈、跳转、恢复现场)
参数类型 无类型 (什么都能传,容易出错) 强类型 (类型不匹配会报错)
副作用 极大 (如 a++ 被执行多次) 安全 (参数先计算好再传入)
调试 很难 (断点打不到宏内部,看到的代码和执行的不一样) 容易 (可以单步进入)

什么情况用宏?(宏的生存空间)

既然函数这么好,为什么还要宏?因为有些事只有宏能做,函数做不到

1. 想要"获取代码本身的信息"时 (Logging)

函数只能拿到变量的 ,拿不到变量的名字 ,也拿不到这行代码在第几行

  • 场景:打印日志。

代码

  • codeCdownloadcontent_copyexpand_less
plain 复制代码
// 函数做不到打印 "__LINE__"(行号),因为它永远只知道它自己被定义在哪一行
#define LOG_ERROR(msg) printf("Error in file %s at line %d: %s\n", __FILE__, __LINE__, msg)
2. 想要"控制编译器开关"时 (条件编译)

函数是运行时执行的,但有时候我们需要代码压根就不参与编译

  • 场景:跨平台、调试开关。

代码

plain 复制代码
// 平台检测
#ifdef _WIN32
    #define PLATFORM "Windows"
    #define PATH_SEPARATOR '\\' 
#elif __linux__
    #define PLATFORM "Linux"
    #define PATH_SEPARATOR '/'
#elif __APPLE__
    #define PLATFORM "macOS"
    #define PATH_SEPARATOR '/'
#endif
3. 想要"生成代码"或"拼接变量名"时

函数不能凭空创造变量名,但宏可以。

  • 场景:自动生成结构体、反射、路由映射。

代码

plain 复制代码
#define DECLARE_VAR(type, name) type name##_variable = 0;

DECLARE_VAR(int, my); 
// 展开变成: int my_variable = 0;

还有,宏里面 一个# 是转字符串 两个##是拼接。

更多文章,敬请关注gzh:零基础爬虫第一天

相关推荐
Caco.D2 小时前
Aneiang.Pa 高阶用法:动态爬虫 SDK 详解与实战
爬虫·aneiang.pa
有味道的男人2 小时前
淘宝图片搜索(拍立淘)+ 店铺全商品爬虫 深度实战指南(Python)
开发语言·爬虫·python
一招定胜负17 小时前
网络爬虫(第三部)
前端·javascript·爬虫
interception1 天前
爬虫逆向:瑞数5(华能电子)
爬虫
光算科技1 天前
商品颜色/尺码选项太多|谷歌爬虫不收录怎么办
java·javascript·爬虫
是Yu欸1 天前
扫描网站结构的SEO元数据抓取方案
爬虫·seo·亮数据·brightdata
Data_Journal1 天前
Puppeteer vs. Playwright —— 哪个更好?
运维·人工智能·爬虫·媒体·静态代理
啊巴矲1 天前
小白从零开始勇闯人工智能:爬虫初级篇(Selenium库)
爬虫·selenium·测试工具
serve the people1 天前
AI 模型识别 Nginx 流量中爬虫机器人的防御机制
人工智能·爬虫·nginx