Node.js技术原理分析系列——如何在Node.js中新增一个内置模块
本文由体验技术团队曹杨毅原创。
Node.js 是一个开源的、跨平台的JavaScript运行时环境,它允许开发者在服务器端运行JavaScript代码。Node.js 是基于Chrome V8引擎构建的,专为高性能、高并发的网络应用而设计,广泛应用于构建服务器端应用程序、网络应用、命令行工具等。
本系列将分为9篇文章为大家介绍 Node.js 技术原理:从调试能力分析到内置模块新增,从性能分析工具 perf_hooks 的用法到 Chrome DevTools 的性能问题剖析,再到 ABI 稳定的理解、基于 V8 封装 JavaScript 运行时、模块加载方式探究、内置模块外置以及 Node.js addon 的全面解读等主题,每一篇都干货满满。
本文内容为本系列第2篇,以下为正文内容。
前言
作为 Node.js 的使用者,想必同学们对"内置模块"这个概念并不陌生:Node.js 内置模块也叫核心模块,预置在 Node.js 运行时中,这些内置模块不需要额外下载安装,可以在 js 代码中通过 require 引入。
常用的内置模块包括 fs、http 等
const fs = require("fs"); const http = require("http"); const url = require("url");
对于这些天天使用的功能,大家有没有好奇过它们是怎么开发出来的呢?
为了更好地了解 Node.js 的底层实现,我把这套流程自己走了一遍。这里可以跟大家分享一下:如何基于 Node.js 开源代码定制开发,添加一组新的内置模块。
所有复杂的功能都是建立在简单的基础之上的,因此我们用作演示的这组内置模块不需要太复杂。我们就实现一个往标准输出流输出信息的功能吧,类似于 console.log() 功能。
我们要实现的效果是这样的:我们的内置模块开发完成之后,我们可以通过这样的一段 js 代码对它进行调用
const my_console = require("my_console"); class a { constructor() { this.a = 1; } } let b = new a(); arr = [1, 2, 3, 4, 5]; my_console.log("Hello World!", 111111, b, arr);
调用的结果是这样的
实现步骤
1、编写核心源码
我们选用 Node.js 22.7.0 源码进行改造,在 Node.js 的 src 文件夹下新增一个源码文件 my_console.cc,在这个文件中编写我们的 C++ 业务代码,实现我们上面提到的功能。
首先引入我们所需要使用的头文件。
为了让我们的程序具备一定的跨平台能力,我们在这里还写了一些条件编译宏,根据平台的不同而使用不同的头文件。
#include "env-inl.h" #include "node_external_reference.h" #include "string_bytes.h" #ifdef __MINGW32__ # include <io.h> #endif // __MINGW32__ #ifdef __POSIX__ # include <climits> // PATH_MAX on Solaris. #endif // __POSIX__ #include <array> #include <cerrno> #include <cstring>
在编写业务代码之前,我们需要先定义一个命名空间(嵌套在 node 命名空间下,Node.js 里面现有的内置模块普遍遵循这个习惯)
namespace node { namespace my_console {
由于我们要使用一些来自其他命名空间的数据类型,所以我们将它们引入进来
using v8::Array; usingv8::ArrayBuffer; usingv8::Boolean; usingv8::Context; usingv8::Float64Array; usingv8::FunctionCallbackInfo; usingv8::Int32; usingv8::Integer; usingv8::Isolate; usingv8::Local;
定义输出内容到控制台的函数 Log ,并且在函数中初始化我们要输出到控制台的字符串 str
static void Log(const FunctionCallbackInfo<Value>& args) { Isolate* isolate = args.GetIsolate(); Local<Context> context = isolate->GetCurrentContext(); std::string str = "";
接下来是实现函数的内容:根据入参类型的不同,进行相应的处理,最后将其格式化输出到控制台
for(int i = 0; i < args.Length(); i++) { Local<Value> arg = args[i]; if(arg.IsEmpty()) { str += "undefined"; } elseif(arg->IsNull()) { str += "null"; } elseif(arg->IsTrue()) { str += "true"; } elseif(arg->IsFalse()) { str += "false"; } elseif(arg->IsInt32()) { str += std::to_string(arg->Int32Value(context).ToChecked()); } elseif(arg->IsNumber()) { str += std::to_string(arg->NumberValue(context).ToChecked()); } elseif(arg->IsString()) { String::Utf8Valuevalue(isolate, arg); str += *value; } elseif(arg->IsArray()) { Local<Array> array = Local<Array>::Cast(arg); str += "["; for(int i = 0; i < array->Length(); i++) { if(i > 0) { str += ", "; } Local<Value> element = array->Get(context, i).ToLocalChecked(); if(element->IsInt32()) { str += std::to_string(element->Int32Value(context).ToChecked()); } elseif(element->IsNumber()) { str += std::to_string(element->NumberValue(context).ToChecked()); } elseif(element->IsString()) { String::Utf8Valuevalue(isolate, element); str += *value; } } str += "]"; } elseif(arg->IsObject()) { v8::Local<v8::String> tmp = v8::JSON::Stringify(context, arg).ToLocalChecked(); v8::String::Utf8Valuevalue(isolate, tmp); str += *value; } }
接下来就是我们的输出语句,这里我们多打印一些东西,把参数个数和内容都给打印出来
int length = args.Length(); printf("Number of arguments: %d\n", length); printf("content: %s\n", str.c_str()); }
最后我们把 Initialize
和 RegisterExternalReferences
这 2 个函数实现出来,要注册一个模块必须要有这 2 个函数。
都是一些模板化的代码了,照着原有的模块复制一份进行修改就行
void Initialize(Local<Object> target, Local<Value> unused, Local<Context> context, void* priv) { Environment* env = Environment::GetCurrent(context); SetMethod(context, target, "log", Log); target ->Set(context, FIXED_ONE_BYTE_STRING(env->isolate(), "isBigEndian"), Boolean::New(env->isolate(), IsBigEndian())) .Check(); } voidRegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(Log); } } // namespace my_console } // namespace node NODE_BINDING_CONTEXT_AWARE_INTERNAL(my_console, node::my_console::Initialize) NODE_BINDING_EXTERNAL_REFERENCE(my_console, node::my_console::RegisterExternalReferences)
现在核心的源码文件已经处理完成,接下来处理一些外围的文件。
2、处理外围文件
为了让模块能正确注册,我们需要改 NODE_BUILTIN_STANDARD_BINDINGS 这个宏,把我们新增的模块也填进去
src/node_binding.cc
#define NODE_BUILTIN_STANDARD_BINDINGS(V) \ V(async_context_frame) \ // ...... 为节省篇幅,省略中间内容 V(worker) \ V(zlib) \ V(my_console) // 新增 my_console
所有的内置模块都会在 C++ 的基础上加一个 js 的封装层(哪怕你不需要封装任何操作),这样它才能被用户以 require 语句导入,这个步骤我们也不能省略。
lib/my_console.js
'use strict'; const my_console = internalBinding('my_console'); module.exports = { log: my_console.log };
改完代码文件之后,最后还要修改构建配置文件。在 node_sources
字段中添加一项:'src/my_console.cc'
。只有这样做,我们新增的这个源码才会被编到 node 里面。
node.gyp
'src/tty_wrap.cc', 'src/udp_wrap.cc', 'src/util.cc', 'src/uv.cc', 'src/my_console.cc', # 新增这一项
到这里,涉及的 4 个文件均已完成新增/修改。
编译
编译环境: ubuntu 22.04 x64
编译命令:
./configure make make install
验证结果
执行测试文件
node mytest/test.js
测试成功
完整代码
完整的修改内容可以查看这个 commit: https://github.com/caoyangyicn/my_console/commit/17b418a4daedd3639116fcc9ea1afcdadc1964d5
下一节,我们将讲解 Node.js 的 perf_hooks 模块作用和用法,请大家持续关注本系列内容~学习完本系列,你将获得:
- 提升调试与性能优化能力
- 深入理解模块化与扩展机制
- 探索底层技术与定制化能力
关于OpenTiny
欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:https://opentiny.design
OpenTiny 代码仓库:https://github.com/opentiny
TinyVue 源码:https://github.com/opentiny/tiny-vue
TinyEngine 源码: https://github.com/opentiny/tiny-engine
欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~ 如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
天天 AI-20250219
官宣!OpenAI前CTO新公司:北大校友翁荔加盟,创始29人2/3来自OpenAI 前OpenAI首席技术官Mira Murati宣布成立新公司——Thinking Machines Lab。该团队由29名成员组成,其中三分之二来自OpenAI,包括前研究副总裁Barret Zoph和联合创始人John Schulman。Mira Murati担任CEO,团队的目标是帮助人们调整AI系统以满足特定需求,开发强大的基础模型,并培养开放的科学文化。Thinking Machines Lab旨在构建一个人人都能获得知识和工具的未来,让AI能够为人类的独特需求服务。该团队强调科学共享的重要性,计划定期发布技术博客和论文,以促进研究文化的改善。来源原文 ChatGPT后训练方法被OpenAI离职联创公开,PPT全网转~ 离开OpenAI的John Schulman和Barret Zoph公开了ChatGPT后训练方法的PPT,分享了他们在斯坦福的演讲内容。后训练阶段是模型开发的最后一步,旨在让模型更像助手,确保其适合实际生产环境。PPT中详细介绍了后训练的三个主要组成部分:监督微调、奖励模...
- 下一篇
百度网盘防雪崩架构实践
导读 大模型在研发效能领域代码生成方面发挥了越来越大的作用 而大模型的预训练依赖大量的精标代码,这些精标数据必须是比较好的工程实践代码 这些比较好的工程实践代码,需要大量的技术沉淀,包括工程架构,代码架构等多纬度,涉及性能、可用性、扩展性、安全等方向 百度网盘有不少比较好的工程实践,本文主要是介绍百度网盘工程架构中的防雪崩架构 抛砖引玉,与大家一起探讨什么才是优秀的工程实践,为大模型的落地提供坚实的数据基础 01 背景 1.1 百度网盘业务背景介绍 简要介绍一下百度网盘的业务背景 外部视角:用户规模超过10亿,单纯外部用户请求日pv过千亿; 内部视角:实例数超过60w+个,模块数规模过千,单个功能最多涉及100+个模块节点。 在复杂的链路+高并发情况下,短暂的异常就会使得整个系统出现雪崩,即使异常消失也无法自愈,对用户产生不好的用户体验。 于是百度网盘服务端工程方向设计了一套防雪崩的机制,本文先介绍雪崩的技术背景,再介绍相关的解决方案。 1.2雪崩发生的背景介绍 雪崩是如何发生的?简单的说,某种场景下,触发一个服务的处理能力(容量)不足而拒绝导致请求失败,而它的上游因为请求失败而重试,...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Hadoop3单机部署,实现最简伪集群
- CentOS8编译安装MySQL8.0.19
- CentOS8安装Docker,最新的服务器搭配容器使用
- Docker安装Oracle12C,快速搭建Oracle学习环境
- 设置Eclipse缩进为4个空格,增强代码规范