举个栗子:
// a.js
module.exports = function(){
dosomething();
}
// b.js
module.exports = require(a);
// c.js
console.log(require(b));
b 是项目 c 依赖的一个工具模块,b 依赖 a。希望只在项目 c 中,b 调用 a 时,a 的函数里能注入一些方法 injectSomething()
期望:hack 之后 c 的输出
function(){
injectSomething();
dosomething();
}
具体案例比如:在做个人自动化工具时,需要 mock 一些工具的手动输入;在本地构建时,需要修改通用的构建流程
主要方法
利用模块 cache 篡改模块对象属性
在模块 a 的类型是 object 的时候,可以在自己的项目 c 中提早 require 模块 a,按照你的需求修改一些属性,这样当模块 b 再去 require 模块 a 时,从缓存中取出的模块 a 已经是被修改过的了
// c.js
const a = require(a);
let oldp = a.p;
a.p = function(...args){
injectSomething();
oldp.apply(this, args);
}
require(b);
缺陷:在某些模块属性是动态加载的情况,不是那么灵敏,而且只能篡改引用对象。但大部分情况下还是能够满足需求的。
修改require.cache
在遇到模块暴露的是非对象的情况,就需要直接去修改 require 的 cache 对象了
//a.js 暴露的非对象,而是函数
module.exports = function(){
doSomething();
}
//c.js
const aOld = require(a);
let aId = require.resolve(aPath);
require.cache[aId] = function(...args){
injectSomething();
aOld.apply(this, args);
}
require(b);
(注意点)
require.cache 是一个 key、value 的 map,key 看上去是模块所在的绝对路径,然而是不能用绝对路径直接去用的,需要 require.resolve 来解析路径,解析后才是 cache 中正确的 key 格式
下面对比下区别:
// 模块的绝对路径
/Users/kino/.def/def_modules/.builders/@ali/builder-cake-kpm/node_modules/@ali/builder-cake-kpm/node_modules/@ali/cake-webpack-config/index.js
// 用 require.resolve 转义后的结果
/Users/kino/.def/def_modules/.builders/@ali/builder-cake-kpm/node_modules/.0.16.23@@ali/cake-webpack-config/index.js
3.修改 require
这种方法是直接去代理 require ,是最稳妥的方法,但是侵入性相对来说比较强。Node.js 文件中的 require 其实是在 Module 的原型方法上,即 Module.prototype.require
const Module = require('module');
const _require = Module.prototype.require;
Module.prototype.require = function(...args){
let res = _require.apply(this, args);
if(args[0] === 'a') { // 只修改a模块内容
injectSomething();
}
return res;
}
缺陷:对整个 Node.js 进程的 require 操作都具有侵入性
判断一个文件是否是被直接运行
require.main === module
完全符合CommonJS规范的包目录应该包含如下这些文件
- package.json:包描述文件。
- bin:用于存放可执行二进制文件的目录。 lib:用于存放JavaScript代码的目录。
- doc:用于存放文档的目录。
- test:用于存放单元测试用例的代码。
可以看到,CommonJS包规范从文档、测试等方面都做过考虑。当一个包完成后向外公布时, 用户看到单元测试和文档的时候,会给他们一种踏实可靠的感觉
```.node文件的加载过程``` 1 第一个步骤是调用uv_dlopen()方法去打开动态链接 库 2 第二个步骤是调用uv_dlsym()方法找到动态链接库中通过NODE_MODULE宏定义的方法地址 >这 两个过程都是通过libuv库进行封装的 >在*nix平台下实际上调用的是dlfcn.h头文件中定义的 dlopen()和dlsym()两个方法;在Windows平台则是通过LoadLibraryExW()和GetProcAddress()这两 个方法实现的,它们分别加载.so和.dll文件(实际为.node文件) >
node中的核心库
1 GYP项目生成工具 一个专有的扩展构建工具node-gyp,这 个工具通过npm install -g node-gyp这个命令即可安装。
2 V8引擎C++库 可以实现JavaScript 与C++的互相调用
3 libuv库 它是Node自身的动力来源之二 。libuv封装的功能包括事件循环
、文件操作
等
4 Node内部库 写C++模块时,免不了要做一些面向对象的编程工作,而Node自身提供了 一些C++代码,比如node::ObjectWrap类可以用来包装你的自定义类,它可以帮助实现对 象回收等工作。
5 其他库 其他存在deps目录下的库在编写扩展模块时也许可以帮助你,比如zlib、openssl、 http_parser等。
一个平台下的.node文件在另一个平台下是无法加载执行的,必须重新用各 自平台下的编译器编译为正确的.node文件。
node的内建模块
buffer、crypto、evals、fs、os
等模块都是部分通过C/C++编写的。
在Node的所有模块类型中,存在着一种依赖层级关系,即文件模块可能会依 赖核心模块(js编写的),核心模块可能会依赖内建模块(C/C++编写的)。
JavaScript模块的编译
事实上,在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加 了(function (exports, require, module, __filename, __dirname) {\n,在尾部添加了\n});。 一个正常的JavaScript文件会被包装成如下的样子:
(function (exports, require, module, __filename, __dirname) {
});
nodejs中的文件加载规则
.js文件 通过fs模块同步读取文件后编译执行。
.node文件 这是用C/C++编写的扩展文件,通过process.dlopen()方法加载最后编译生成的文件。
.json文件 通过fs模块 同步读取文件后,用JSON.parse()解析返回结果。
其余扩展名文件 它们都被当做.js文件载入。
nodejs 中的模块路径查找过程
1 当前文件目录下的node_modules目录。
2 父目录下的node_modules目录。
3 父目录的父目录下的node_modules目录。 沿路径向上逐级递归,直到根目录下的node_modules目录。
技巧篇幅
- 如果是.node和.json文件,在传递给require() 的标识符中带上扩展名,会加快一点速度
- 同步配合缓存,可以大幅度缓解Node 单线程中阻塞式调用的缺陷。
兼容多种模块规范
能够兼容Node、AMD、CMD以及常见的浏览器 环境中:
;(function (name, definition) {
// 检测上下文环境是否为AMD或CMD
var hasDefine = typeof define === 'function',
// 检查上下文环境是否为Node
hasExports = typeof module !== 'undefined' && module.exports;
if (hasDefine) {
// AMD环境或CMD环境 define(definition);
} else if (hasExports) {
// 定义为普通Node模块 module.exports = definition();
} else {
// 将模块的执行结果挂在window变量中,在浏览器中this指向window对象 this[name] = definition();
}
})('hello', function () {
var hello = function () {};
return hello; });
node
处理一对多并行的情况
var after = function (times, callback) {
var count = 0, results = {};
return function (key, value) {
results[key] = value;
count++;
if (count === times) {
callback(results);
}
};
};
var done = after(times, render);
var emitter = new events.Emitter();
var done = after(times, render);
emitter.on("done", done);
emitter.on("done", other);
fs.readFile(template_path, "utf8", function (err, template) { emitter.emit("done", "template", template);
});
db.query(sql, function (err, data) {
emitter.emit("done", "data", data); });
l10n.get(function (err, resources) { emitter.emit("done", "resources", resources);
});
多对一并行
var after = function (times, callback) {
var count = 0, results = {};
return function (key, value) {
results[key] = value;
count++;
if (count === times) {
callback(results);
}
};
};
var done = after(times, render);
var emitter = new events.Emitter();
var done = after(times, render);
emitter.on("done", done);
emitter.on("done", other);
fs.readFile(template_path, "utf8", function (err, template) { emitter.emit("done", "template", template);
});
db.query(sql, function (err, data) {
emitter.emit("done", "data", data); });
l10n.get(function (err, resources) { emitter.emit("done", "resources", resources);
});
其实感觉 webpack 中的 tapable 是一个很不错的任务流工具类 适用于串行
异步 task
//异步流
class AsyncHook{
constructor(){
this.hooks = [];
}
/**
* 订阅
* name 当前订阅者
* fn 收到消息后执行的函数
*/
tapAsync(name,fn){
/**
* fn 函数接受四个参数
* tag |string当前执行者
* task|string任务
* result |any 前一个执行结果
* next | Functon下一个任务 当前的执行结果传给next
**/
this.hooks.push({name,fn});
}
/**
*args[0] | any 任务
*args[1] | Function 最终的回调
*/
callAsync(...args){
const task = args[0];
const done = args[args.length-1];
let index = 0;
const next = (result)=>{
this.hooks[index]?this.hooks[index]["fn"](this.hooks[index]["name"],task,result,next):done(task,result);
index++;
}
next();
}
}