>

怎么样 hack Node.js 模块?

- 编辑:至尊游戏网站 -

怎么样 hack Node.js 模块?

如何 hack Node.js 模块?

2016/10/28 · JavaScript · NodeJS

初藳出处: 天猫前端团队(FED)- 宣予   

图片 1

干什么要去 hack?

在事情支付进度中,往往会依据一些 Node.js 模块,hack 这几个 Node.js 模块的重中之重指标是在不更改工具源码的情形下,点窜一些特定的效应。或者会是出于以下三种情景的虚构:

  1. 接连几天来存在一些特有的地点需要,不必然能作为工具的通用需要来暴光平常的 API 给越来越多的客商。
  2. 暂且且火急的需要,提 P凯雷德 已经来比不上了。
  3. 缘何不直接去改源码?考虑到工具会不许期进级,想行使工具的新星天性,改源码可维护性太差。

期望

举个栗子:

JavaScript

// a.js module.exports = function(){ dosomething(); } // b.js module.exports = require(a); // c.js console.log(require(b));

1
2
3
4
5
6
7
8
// 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 的输出

JavaScript

function(){ dosomething(); }

1
2
3
function(){
  dosomething();
}
  • 期望:hack 之后 c 的输出

JavaScript

function(){ injectSomething(); dosomething(); }

1
2
3
4
function(){
  injectSomething();
  dosomething();
}

现实案例举例:在做个人自动化学工业具时,要求 mock 一些工具的手动输入;在地面构建时,要求改良通用的创设流程(前面案例部分会详细说)

重在形式

使用模块 cache 窜改模块对象属性

那是自己最先接纳的方法,在模块 a 的体系是 object 的时候,能够在本人的品类 c 中提前 require 模块 a,根据你的须要改进部分性质,那样当模块 b 再去 require 模块 a 时,从缓存中抽取的模块 a 已然是被涂改善的了。

模块 a、b、c 栗子如下:

JavaScript

// a.js module.exports = { p } // b.js const a = require(a); a.p(); // c.js require(b);

1
2
3
4
5
6
7
8
9
// a.js
module.exports = {
  p
}
// b.js
const a = require(a);
a.p();
// c.js
require(b);

自家想改善 a 的办法 p,在 c 中张开如下改善就能够,而无需直接去更改工具 a、b 的源码:

JavaScript

// c.js const a = require(a); let oldp = a.p; a.p = function(...args){ injectSomething(); oldp.apply(this, args); } require(b);

1
2
3
4
5
6
7
8
// 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 对象了。关于匡正 require.cache 的平价,会在后头的规律部分详细说,先来简单的讲下操作:

JavaScript

//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);

1
2
3
4
5
6
7
8
9
10
11
12
//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,例如热加载。

修改 require

这种办法是一贯去代理 require ,是最伏贴的章程,不过侵入性相对来讲比较强。Node.js 文件中的 require 其实是在 Module 的原型方法上,即 Module.prototype.require。后边会详细说,先轻易说下操作:

JavaScript

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; }

1
2
3
4
5
6
7
8
9
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 操作都享有侵入性。

有关原理

node的运转进程

我们先来会见在运作 node a.js 时爆发些什么?node源码
图片 2

上图是node运行 a.js 的四个核心流程,Node.js 的起步程序 bootstrap_node.js 是在 node::LoadEnvironment 中被马上实行的,bootstrap_node.js 中的 startup() 是包裹在一个无名函数里面包车型地铁,所以在一遍实践 node 的一举一动中 startup() 只会被调用了壹回,来保管 bootstrap_node.js 的所实践的兼具正视只会被加载一回。C++ 语言部分中:

JavaScript

//node_main.cc 如若在win意况进行wmain(),unix则试行main(),函数最终都进行了node::Start(argc, argv) #ifdef _WIN32 int wmain() #else int main() #endif //node::Start(argc, argv) 提供载入 Node.js 进度的 V8 情况Environment::AsyncCallbackScope callback_scope(&env); LoadEnvironment(&env); //node::LoadEnvironment(Environment* env) 加载 Node.js 环境 Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate()," bootstrap_node.js"); Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//node_main.cc 如果在win环境执行wmain(),unix则执行main(),函数最后都执行了node::Start(argc, argv)  
#ifdef _WIN32
  int wmain()
#else
  int main()
#endif
 
//node::Start(argc, argv) 提供载入 Node.js 进程的 V8 环境
Environment::AsyncCallbackScope callback_scope(&env);
LoadEnvironment(&env);
 
//node::LoadEnvironment(Environment* env) 加载 Node.js 环境
Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate()," bootstrap_node.js");
Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);

bootstrap_node.js 中,会去实践 Module 的静态方法 runMain,而 runMain 中则去施行了 Module._load,也正是模块加载的长河。

JavaScript

// bootstrap_node.js const Module = NativeModule.require('module'); …… run(Module.runMain); // Module.js Module.runMain = function() { Module._load(process.argv[1], null, true); process._tickCallback(); };

1
2
3
4
5
6
7
8
9
// bootstrap_node.js
const Module = NativeModule.require('module');
……
run(Module.runMain);
// Module.js
Module.runMain = function() {
    Module._load(process.argv[1], null, true);
    process._tickCallback();
};

一个历程只设有贰个 cache 对象?

先来探视 module._load 干了怎么?

JavaScript

Module._load = function(request, parent, isMain) { var filename = Module._resolveFilename(request, parent, isMain); var cachedModule = Module._cache[filename]; // get cache if (cachedModule) { return cachedModule.exports; } …… var module = new Module(filename, parent); …… Module._cache[filename] = module; // set cache tryModuleLoad(module, filename); return module.exports; };

1
2
3
4
5
6
7
8
9
10
11
12
13
Module._load = function(request, parent, isMain) {
  var filename = Module._resolveFilename(request, parent, isMain);
  var cachedModule = Module._cache[filename]; // get cache
  if (cachedModule) {
    return cachedModule.exports;
  }
  ……
  var module = new Module(filename, parent);
  ……
  Module._cache[filename] = module; // set cache
  tryModuleLoad(module, filename);
  return module.exports;
};

能够看来,在 load 的二个模块时,会先读缓存 Module._cache,如果未有就能够去 new 三个 Module 的实例,
然后再把实例放到缓存里。由前边的 Node.js 运转进程可以清楚, bootstrap_node.js 中的 startup() 只会实行了一回,当中产生的 Module 对象在全部node进度调用链路中只会存在三个,进而 Module._cache 只有二个。

Module._cacherequire.cache 的关系

能够看下 Module.prototype._compile 这些法子,这里面会对大家写的 Node.js 文件举行四个卷入,注入一些上下文,饱含 require:

JavaScript

var require = internalModule.makeRequireFunction.call(this); var args = [this.exports, require, this, filename, dirname]; var depth = internalModule.requireDepth; var result = compiledWrapper.apply(this.exports, args);

1
2
3
4
var require = internalModule.makeRequireFunction.call(this);
var args = [this.exports, require, this, filename, dirname];
var depth = internalModule.requireDepth;
var result = compiledWrapper.apply(this.exports, args);

而在 internalModule.makeRequireFunction 中大家会开掘

JavaScript

// 在 makeRequireFunction 中 require.cache = Module._cache;

1
2
// 在 makeRequireFunction 中
require.cache = Module._cache;

所以,Module._cacherequire.cache 是同样的,那么我们直接改良 require.cache 的缓存内容,在二个 Node.js 进度里都以一蹴而就的。

require 分歧景色的挂载

最开端自身感觉 require 是挂载在 global 上的,为了图省事,通常用 Node.js repl 来测量检验:

JavaScript

$ node > global.require { [Function: require] resolve: [Function: resolve], main: undefined, extensions: { '.js': [Function], '.json': [Function], '.node': [Function] }, cache: {} }

1
2
3
4
5
6
7
$ node
> global.require
{ [Function: require]
  resolve: [Function: resolve],
  main: undefined,
  extensions: { '.js': [Function], '.json': [Function], '.node': [Function] },
  cache: {} }

能够见到,repl 下,global.require 是存在的,如若感觉能够直接在 Node.js 文件中代理 global.require 那就踩坑了,因为如火如荼旦在 Node.js 文件中利用会开采:

JavaScript

console.log(global.require); // undefined

1
2
console.log(global.require);
// undefined

从上文可以见到,Node.js 文件中的 require 其实是来源于于 Module.prototype._compile 中注入的 Module.prototype.require, 而最后的对准其实是 Module._load,并从未挂载到 module 上下文境况中的 global 对象上。

而 repl 中也可能有 module 实例,于是自身尝试在 repl 中打字与印刷:

JavaScript

$ node > global.require === module.require false

1
2
3
$ node
> global.require === module.require
  false

结果有点出人意料,于是作者延续探求了下。在 bootstrap_node.js 中找到 repl 的调用文件 repl.js

JavaScript

const require = internalModule.makeRequireFunction.call(module); context.module = module; context.require = require;

1
2
3
const require = internalModule.makeRequireFunction.call(module);
context.module = module;
context.require = require;

获得结论:在 repl 中,module.requireglobal.require 最后的调用方法是均等的,只是函数指向分化而已。

注意点

path路径

require.cache 是多个 key、value 的 map,key 看上去是模块所在的相对路线,但是是无法用相对路线直接去用的,必要 require.resolve 来深入分析路线,深入分析后才是 cache 中正确的 key 格式。

上边前蒙受比下分别:

JavaScript

// 模块的相对路线/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

1
2
3
4
5
// 模块的绝对路径
/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

多进度的景况

模块间调用的链路相比长,有希望会新建子进度,须求思量你项目中的入口文件和您必要代理的公文是不是在贰个进程中,轻易的不二等秘书技就是在进口文件和您必要代理的文书打字与印刷pid:

JavaScript

console.log(process.pid)

1
console.log(process.pid)

纵然相仿,那么直接在进口调用前代理就可以,不然事态会更复杂点,要求找到呼应的进度调用处实行代理。

案例

DEF 是天猫商城前端的集成开荒条件,扶持前端模块创建、构建打包、发布等如日中天多级流程。 在偏下案例中,重要 hack 的 Node.js 项目正是 DEF。

点窜输入(prompt)

现象:使用 DEF 创造模块 or 公布模块时

原因:想黄金时代键实现批量成立 or 批量发表,不想手动输入。

减轻进度:以创办模块为例

  • 首先找到 DEF 的入口文件,即叁个 bin 目录下的路径,能够透过那些进口文件不断追溯下去,开掘创制模块的 generator 用的是 yeoman-generator 的方法。对 prompt 的法子举办代理,能够将该基础库提前 require,改正掉其 prompt 的办法就能够。
  • 依赖示例代码(示例只点窜 def add 模块的始建项目,其余输入的点窜方法相像):

JavaScript

#!/usr/bin/env node 'use strict'; require('shelljs/global'); const path = require('path'); const HOME = process.env.HOME; const yeomanRouter = require(path.join(HOME, '.def/def_modules/.generators/@ali/generator-abs-router/node_modules/@ali/generator-abs-router/node_modules/yeoman-generator')); yeomanRouter.generators.Base.prototype.prompt = function(list, callback) { let item = list[0]; let prop = {}; prop[item.name] = 'kissy-pc'; // 让模块类型输入自动为pc callback(prop); }; //require real def path const defPath = which('def').stdout; require(defPath);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env node
 
'use strict';
 
require('shelljs/global');
const path = require('path');
const HOME = process.env.HOME;
 
const yeomanRouter = require(path.join(HOME, '.def/def_modules/.generators/@ali/generator-abs-router/node_modules/@ali/generator-abs-router/node_modules/yeoman-generator'));
 
yeomanRouter.generators.Base.prototype.prompt = function(list, callback) {
  let item = list[0];
  let prop = {};
  prop[item.name] = 'kissy-pc'; // 让模块类型输入自动为pc
  callback(prop);
};
 
//require real def path
const defPath = which('def').stdout;
require(defPath);

点窜构建流程(webpackconfig)

此情此景:二个Taobao的前端组件,须要在运用def当地调试时提前转移多个文件内容。(天猫商城组件的创设会依照组件类型统风姿洒脱营造器,而不是各种组件单独去安插)

原因:常常的话,这种情景能够接纳注释代码大法,本地调节和测量试验时展开注释,发表前干掉。但如此形成代码十分不好看,也轻易招惹误操作。不要紧在地点调节和测量试验的 reflect 进程中动态转变掉就好了。

减轻进程:

  • 追溯 def dev 调用链路,找到最终reflect的文本, 在此个创设器 @ali/builder-cake-kpm 项目里。所采纳的webpack的安顿项在 @ali/cake-webpack-config 下。
  • 将来就是往 webpack 配置项里动态注入一个 webpack loader 的长河了,小编索要的 loader 是多个preLoader,代码特轻易,小编把它身处专业类其他文件里:

JavaScript

module.exports = function(content) { return content.replace('require('./plugin')', "require('./localPlugin')"); };

1
2
3
module.exports = function(content) {
    return content.replace('require('./plugin')', "require('./localPlugin')");
};
  • @ali/cake-webpack-config 暴光的是个函数而非对象,所以必需从 require 入手了,最终附上案例的代理进度:

JavaScript

#!/usr/bin/env node 'use strict'; require('shelljs/global'); const path = require('path'); const HOME = process.env.HOME; const CWD = process.cwd(); const cakeWcPath = path.join(HOME, '.def/def_modules/.builders/@ali/builder-cake-kpm/node_modules/@ali/builder-cake-kpm/node_modules/@ali/cake-webpack-config'); const preLoaderPath = path.join(CWD, 'debug/plugin_compile.js'); // 注入的loader路径 const cakeWebpackConfig = require(cakeWcPath); const requireId = require.resolve(cakeWcPath); require.cache[requireId].exports = (options) => { if (options.callback) { let oldCb = options.callback; options.callback = function(err, obj) { obj.module.preLoaders = [{ 'test': /index.js$/, 'loader': preLoaderPath }]; oldCb(err, obj); } } cakeWebpackConfig(options); } //require real def path const defPath = which('def').stdout; require(defPath);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env node
'use strict';
 
require('shelljs/global');
const path = require('path');
const HOME = process.env.HOME;
const CWD = process.cwd();
 
const cakeWcPath = path.join(HOME, '.def/def_modules/.builders/@ali/builder-cake-kpm/node_modules/@ali/builder-cake-kpm/node_modules/@ali/cake-webpack-config');
const preLoaderPath = path.join(CWD, 'debug/plugin_compile.js'); // 注入的loader路径
const cakeWebpackConfig = require(cakeWcPath);
const requireId = require.resolve(cakeWcPath);
require.cache[requireId].exports = (options) => {
  if (options.callback) {
    let oldCb = options.callback;
    options.callback = function(err, obj) {
      obj.module.preLoaders = [{
        'test': /index.js$/,
        'loader': preLoaderPath
      }];
      oldCb(err, obj);
    }
  }
  cakeWebpackConfig(options);
}
 
//require real def path
const defPath = which('def').stdout;
require(defPath);

结束语

去 hack 二个 Node.js 模块,需求对该 Node.js 模块的调用链路有肯定的领会,在数不胜数地方下,不必然是最优的章程,但也不失为如日方升种缓慢解决方案。有意思的是,Node.js 源码中实际有后生可畏行那样的申明:

JavaScript

// Hello, and welcome to hacking node.js! // some descriptions

1
2
// Hello, and welcome to hacking node.js!
// some descriptions

So, just hacking for fun!

1 赞 2 收藏 评论

图片 3

本文由IT-综合发布,转载请注明来源:怎么样 hack Node.js 模块?