1. Introduction
In daily front-end /Node development, require is one of the most commonly used apis, and understanding the logic behind it can help with daily development and troubleshooting. But when we read the source code, we tend to get bored. After reading it, it looks like we know how the code works, but it’s hard to get out of the process to hack it and supplement it.
This article does not explain the require from scratch is invoked, it all happens, if you are interested, can go to the source code (the node/lib/internal/modules/CJS/loader. Js) in reading, or go to online search keyword to read other people write source code parsing. The purpose of this article is to show readers whether they can grasp the key points of internal implementation and achieve something interesting and meaningful after they have a general understanding of what is happening behind require.
Ps: You don’t need to have read the source code in advance, but at least you have used the require API, and it is better if you know some of the internals.
2. Foreplay work
To get started, let’s walk through a simple line of code and use a flowchart to document what happens when Node runs:
-
The code is as follows:
const xx_mod = require(“xx_mod”);
-
The flow chart is as follows:
-
Note in Node source code:
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call //
NativeModule.prototype.compileForPublicLoader()
and return the exports.// 3. Otherwise, create a new module for the file and save it to the cache. // Then have it load the file contents before returning its exports // object.
From the flow chart, when we need to load a module, we will go to the cache first. If it’s not in the cache, use the appropriate loader to load it and store it in the cache. This article explores some interesting things around caches and loaders.
3. Content mining
Topic 1: Code hot updates
According to my personal understanding of hot update, hot update refers to the realization of updating the file content without restarting the application.
For example, suppose you write a module called xx_mod.js that says:
//xx_mode.jsconst _str = "hello world"
module.export = { sayHi: ()=>{ return _str; }}
Copy the code
When you load the module in your application and use its exported sayHi, it will return “Hello world”, but now you have a new idea and want it to return “hello 🔨”. Usually, you modify xx_mode.js and restart the application. This will return “hello 🔨”, but hot updates can be done without restarting the app, or “hello 🔨” after you have modified xx_mode.js.
Going back to section 2, we have an idea of a cache when we mention that a module is loaded. This is incompatible with implementing hot updates. If xx_mod.js is loaded before, it will be cached without reboot. How do you implement the update? Smart students must have thought of a way: delete cache.
Roughly can post a truncated version of the source code:
Module._load = function(request, parent, isMain) { //... const cachedModule = Module._cache[filename]; if (cachedModule ! == undefined) { updateChildren(parent, cachedModule, true); if (! cachedModule.loaded) { const parseCachedModule = cjsParseCache.get(cachedModule); if (! parseCachedModule || parseCachedModule.loaded) return getExportsForCircularRequire(cachedModule); parseCachedModule.loaded = true; } else { return cachedModule.exports; }} // mod = loader returns; // Module._cache[filename] = mod; // return module.exports = mod; }Copy the code
From this line of code, the simplest solution we can think of is:
-
Using file listening, you can listen to the modified file and obtain filename.
-
Delete Module._cache[filename];
-
The require again (filename)
But there is also some deep pit, may lead to memory leaks, interested can go to take a look at this article: zhuanlan.zhihu.com/p/34702356
Topic 2: Hack module loader
This is a question that my colleagues asked me when I was doing test-related work in my previous company. The general content is as follows:
fucntion _private(){ //do something } module.exports = function _public(){ // .... _private(); / /... }Copy the code
He is now required to unit test his js modules and coverage 100%, but he does not want to expose _private.
If it’s convenient, just use the NPM package Rewire, which can do all of this. If you are interested, you can read the code inside it. It is actually a nesting of the native module loader to meet its own needs. To make it easier to read, I’ve posted a runnable version of the code:
You can also download the code here :github.com/huenchao/si…
. ├ ─ ─ __test__. Js / / test file ├ ─ ─ libhack │ └ ─ ─ index. The js / / lite version rewire └ ─ ─ if js/js library/colleagues wroteCopy the code
library.js :
//library.js
function _private(){
return "_private";
}
module.exports = function _public(){
//... whatever
return _private();
}
Copy the code
__ test__.js :
const assert = require("assert"); const lib = require("./libhack"); Console.log (lib("./library.js").getPrivatemember ("_private")()); assert(lib("./library.js").getPrivateMember("_private")() === '_private');Copy the code
libhack/index.js:
const Module = module.constructor;
function _getPrivateMember() {
arguments.varName = arguments[0];
if (arguments.varName && typeof arguments.varName === "string") {
return eval(arguments.varName);
} else {
throw new TypeError("__get__ expects a non-empty string");
}
}
function _hackCode(){
return "\n Object.defineProperty(module.exports, \"getPrivateMember\", {enumerable: false, value: " + _getPrivateMember.toString() + ", " + "writable: true}); ";
}
function _fakeModule(parentModule, id) {
const targetPath = Module._resolveFilename(id, parentModule);
const targetModule = new Module(targetPath, parentModule);
Module.wrapper[1] = _hackCode() + Module.wrapper[1];
targetModule.load(targetModule.id);
return targetModule.exports;
}
function injectLib(fileName) {
return _fakeModule(module.parent, fileName)
}
module.exports = injectLib;
Copy the code
The result of this operation is:
Output :_private and assert passes.Copy the code
4. At the end
That’s all for this article. There’s a lot to think about caching and hack loader triggers, because JS is runtime and context escape is easy to implement. For example, we can also use the hack loader to achieve file encryption, right? Unconstrained, first have ideas to improve the scheme, keep a little curiosity, leave us to think 🤔.
5. Eggs
Our department hires front-end, Noder. 15 HC, P5 + recruit, welcome interested partners to come to me, I can provide free human 1V1 interview coaching, base Hangzhou.
My personal wechat :18258199330
Resumes can also be sent to :[email protected]