1 如何监听 Node.js 的所有函数
这是一次危险的探索,但是或许某些场景下可以用到。主要想做的事情是劫持所有的 Node.js 函数,在函数执行前后,插入钩子做些事情。但是由于场景很多而且负责,劫持的风险非常高,如果你使用以下代码有问题,可以提个 issue。以下代码可以通过预加载方式加载或者在你的代码执行前加载。
module-wrap.js
const {
Module } = require('module');
function before(...args) {
console.log(`before call function args: ${
args}`);
}
function after(...args) {
console.log(`after call function result: ${
args}`)
}
const originRequire = Module.prototype.require;
// hack to make console init
console.log('');
function newRequire(...args){
let exports = originRequire.call(this, ...args);
function patch(originFunc, key = originFunc.name) {
function dummy(...args) {
// you can do something before the function will be executed
before([key, ...args]);
let result;
// if the function call by new, we call by new too
if (new.target) {
result = new originFunc(...args);
// make the constructor point to new.target instead of originFunc because new.target maybe be a subclass of originFunc
result.constructor = new.target;
} else {
result = originFunc.call(this, ...args);
}
const params = [key];
if (result) {
params.push(result);
}
// you can do something after the function have executed
after(params);
return result;
}
// we need merge the fields which is writable of originFunc into dummy
for (const [key, descriptionInfo] of Object.entries(Object.getOwnPropertyDescriptors(originFunc))) {
if (descriptionInfo.writable) {
Object.defineProperty(dummy, key, descriptionInfo);
}
}
// change the function name to the name of originFunc
Object.defineProperty(dummy, 'name', {
configurable: true, value: originFunc.name });
Object.defineProperty(dummy, 'name', {
configurable: false });
// the prototype of dummy need point to originFunc.prototype
dummy.prototype = originFunc.prototype;
return dummy;
}
// wrapper all functions in export, but now we don not handle the exports recursively
if (Object.prototype.toString.call(exports) === '[object Object]') {
for (const [key, value] of Object.entries(exports)) {
if (typeof value === 'function') {
exports[key] = patch(value, key);
}
}
} else if (Object.prototype.toString.call(exports) === '[object Function]') {
exports = patch(exports);
}
return exports;
}
Module.prototype.require = newRequire;
测试例子。
server.js
const http = require('http');
http.createServer((req, res) => {
res.end('ok');
}).listen(8888);
执行 node -r ./module-wraper.js server.js 将会看到输出
before call function args: createServer,(req, res) => {
res.end('ok');
}
after call function result: createServer,[object Object]
你可以在钩子里做你想做的事情。
2 如何实现直接执行 ts 代码
ts-node 相信很多同学都使用过,它可以直接执行 ts 模块。下面的代码同样可以做到。
const {
Module } = require('module');
const fs = require('fs');
const path = require('path');
const ts = require('typescript');
const {
compileFunction } = process.binding('contextify');
Module._extensions['.ts'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
const {
outputText } = ts.transpileModule(content, {
compilerOptions: {
module: ts.ModuleKind.CommonJS }});
const result = compileFunction(
outputText,
filename,
0,
0,
undefined,
false,
undefined,
[],
[
'exports',
'require',
'module',
'__filename',
'__dirname',
]
);
result.function.call(this, module.exports, (...args) => module.require(...args), module, filename, path.dirname(filename));
};
原理很简单,主要是给 Node.js 增加一个 ts 模块的 加载器,在加载器里通过 typescript 包编译 ts 成 js,然后再调用 V8 的 compileFunction 执行 js。
3 如何写一个 js loader
Node.js 的某些框架的实现模块是在启动前会加载所有的模块成一个树状的结果,下面代码是实现这个 loader 的逻辑。
const fs = require('fs');
const {
relative } = require('path');
function load() {
return new Promise((resolve, reject) => {
const root = process.cwd() + '/a';
const fileTree = {
};
const REGEXP = /\.(js|json|node)$/;
const filters = ['node_modules', '__tests__'];
let request = 0;
let done = false;
function _load(currentPath) {
request++;
fs.readdir(currentPath, (error, dirOrFiles) => {
request--;
if (error) {
console.error(error);
if (!done) {
done = true;
reject(error);
}
} else if (dirOrFiles.length) {
const absolutePaths = dirOrFiles.filter( (file) => !filters.includes(file) ).map((file) => `${
currentPath}/${
file}`);
for (let i = 0; i < absolutePaths.length; i++) {
const absolutePath = absolutePaths[i];
request++;
fs.stat(absolutePath, (error, stat) => {
request--;
if (error) {
console.error(error);
if (!done) {
done = true;
reject(error);
}
} else {
if (stat.isDirectory()) {
_load(absolutePath);
} else {
try {
if (REGEXP.test(absolutePath)) {
const absolutePathWhithoutExt = absolutePath.replace(REGEXP, '');
const relativePathWhithoutExt = relative(root, absolutePathWhithoutExt);
const paths = relativePathWhithoutExt.split('/');
let currentNode = fileTree;
for (let j = 0; j < paths.length - 1; j++) {
const path = paths[j];
if (typeof currentNode[path] === 'object' && currentNode[path] !== null) {
currentNode = currentNode[path];
} else {
currentNode = currentNode[path] = {
};
}
}
currentNode[paths[paths.length - 1]] = require(absolutePath);
}
} catch(e) {
console.error(e);
if (!done) {
done = true;
reject(e);
}
}
}
}
if (!request && !done) {
done = true;
resolve(fileTree);
}
});
}
}
if (!request && !done) {
resolve(fileTree);
}
});
}
_load(root);
});
}
load().then(console.log).catch(console.error);
利用异步读取的方式提高速度。
github 地址:
- https://github.com/theanarkh/Node.js-Function-Wrapper
- https://github.com/theanarkh/tiny-ts-node
- https://github.com/theanarkh/Node.js-Loader