Some times ago (I don’t really remember when), working on a Node.Js project, I realised something about require
…
The situation …
Imagine we have some datas in a Json file we want to access in different modules.
{
"version":"0.1",
"name":"MyApplication"
}
This datas.json
is quite short, but for explaining the situation it is more than enough !
Now the code :
var module1 = require('./module1.js'),
module2 = require('./module2.js');
module1.log();
module2.log();
var myDatas = require('./datas.json');
module.exports = {
log:function(){
console.log(myDatas);
}
}
var myDatas = require('./datas.json');
module.exports = {
log:function(){
console.log(myDatas);
}
}
As you can see, module1
and module2
are exactly same.
So if we execute the code, we will have the following output :
⇒ node index.js
{ version: 'O.1', name: 'MyApplication' }
{ version: 'O.1', name: 'MyApplication' }
Ok, now imagine module1
will modify some value before logging :
var myDatas = require('./datas.json');
module.exports = {
log:function(){
lowerCaseName() (1)
console.log(myDatas);
}
}
function lowerCaseName(){
myDatas.name = myDatas.name.toLowerCase();
}
1 | We just add this simple function that will lowerCase the name. |
In my idea, this should not affect the module2
as they should be independant, so when I execute the code, here is the output :
⇒ node index.js
{ version: 'O.1', name: 'myapplication' }
{ version: 'O.1', name: 'myapplication' }(1)
1 | Damned ! |
Why
Actually the reason is quite simple, when you require the file datas.json
, the require
function will create the object and store it in the require.cache
object, then next time you use require('./datas.json')
to retreive the datas, require
will return you the value in that require.cache
object.
So it mean that it gives you the reference to the value, so if you change one property of that value, all other modules are affected as you are actually doing the change on the reference in the cache.
Modules are cached after the first time they are loaded. This means (among other things) that every call to require('foo') will get exactly the same object returned, if it would resolve to the same file.
https://nodejs.org/api/modules.html#modules_caching
Remember that Node.Js is single thread, it mean for example that the require cache is shared accross all requests
Reflections on solutions to have the desired behaviour
Now I will try to make the code work as desired.
Some of the solutions are not solving the problem, but I wanted to try to see how it works …
The Const way … FAIL
First I wanted to use the new ES6 const
instead of var
in the line
// replace
var myDatas = require('./datas.json');
// with
const myDatas = require('./datas.json');
⇒ node index.js
{ version: 'O.1', name: 'myapplication' }
{ version: 'O.1', name: 'myapplication' }(1)
1 | Damned ! |
But actually, const
do not prevent the variable to be modified, but prevent reafectation.
FAIL
The delete way … SUCCESS
With this solution, you will delete
the reference in the require.cache
object before require the datas.json
file like this :
delete require.cache[require.resolve('./datas.json')];
var myDatas = require('./datas.json');
// Code continue ...
As you may not know how your modules are inserted, you need to add the line on every files that need to use the datas.json
file.
In my opinion, this is not very convenient, but it works :
⇒ node index.js
{ version: 'O.1', name: 'myapplication' }
{ version: 'O.1', name: 'MyApplication' } (1)
1 | Great ! |
I Think that this solution is the "Quick And Dirty" solution.
SUCCESS
The Proxy Way … SUCCESS
With this solution I imagine to create a Proxy
around the json datas, and override the set
method, in order to forbid the manipulation of the value.
This approach can be usefull to throw excpetion if someone try to set the property value.
Here is the code :
var datas = require('./datas.json'); (1)
module.exports = new Proxy(datas, {
set:function(){
return; (2)
}
});
1 | This should be the only location in the app where you require the json file. |
2 | Here, we just do nothing, but we could throw exception here to reject any modification of any properties. |
We also need to change the reference in module1
and module2
:
// Replace
var myDatas = require('./datas.json');
// with
var myDatas = require('./datasProxy');
Can you see a big problem ?
YES, the code in module1
should be updated because now we can not set the property (even locally)
Let’s first execute the code without any modification :
⇒ node index.js
{ version: 'O.1', name: 'MyApplication' }
{ version: 'O.1', name: 'MyApplication' }
So there is a problem, the first line should display the text MyApplciation in lowercase.
So let’s edit the code in module1
to have the desired behaviour.
Here is a working code :
var myDatas = Object.assign({}, require('./datasProxy')); (1)
module.exports = {
log:function(){
lowerCaseName()
console.log(myDatas);
}
}
function lowerCaseName(){
myDatas.name = myDatas.name.toLowerCase();
}
1 | We use Object.assign() to create a "copy" of the object in the module1 |
If we look at the output :
⇒ node index.js
{ version: 'O.1', name: 'myapplication' }
{ version: 'O.1', name: 'MyApplication' }
So it works !
But, as the MDN web site says:
"The
Object.assign()
method only copies enumerable and own properties from a source object to a target object."
http://devdocs.io/javascript/global_objects/object/assign
Maybe you should use a "clone" function that allow to clone objects deeply (check this with lodash _.clone()
for example)
I think this solution is not too bad, but the thing is that you delegate to the module the need to create a copy, maybe this suits your needs, or maybe we can do it in the proxy itself.
I think both solution can be justified, you just need to make a choice.
SUCCESS
The Clone Way … SUCCESS
This solution is the solution I considered previously.
So with this solution, the "Proxy" (or you can call it, the "wrapper") will create the copy and returns it to the modules :
So edit the Proxy code, and the modules :
var datas = require('./datas.json');
module.exports = function(){ (1)
return clone(datas);
}
function clone(datas){
return JSON.parse(JSON.stringify(datas)); (2)
}
1 | We export a function that need to be called in module to return a copy of the datas |
2 | Here we use a hack to clone "deeply" an object. |
Then edit the modules to call the exported function
var myDatas = require('./datasProxy')(); (1)
module.exports = {
log:function(){
lowerCaseName()
console.log(myDatas);
}
}
function lowerCaseName(){
myDatas.name = myDatas.name.toLowerCase();
}
1 | Call the function to get the copy. |
var myDatas = require('./datasProxy')(); (1)
module.exports = {
log:function(){
console.log(myDatas);
}
}
1 | Call the function to get the copy. |
Then execute the code :
⇒ node index.js
{ version: 'O.1', name: 'myapplication' }
{ version: 'O.1', name: 'MyApplication' }
Great it works !
This is my favorite solution. |
SUCCESS
The decache way … SUCCESS
This idea was given to me by Jérémy Morin.
Decache is a Node Module, that remove module from the require
cache.
This solution has exactly the same effect than the delete solution I present before. But it make it easier to do.
First you need to install the decache
node module :
npm init (1)
npm install decache --save
1 | This is not necessary if you already initialise a node project. |
Then edit the code :
var module1 = require('./module1.js'),
module2 = require('./module2.js');
module1.log();
module2.log();
var myDatas = require('./datas.json');
module.exports = {
log:function(){
lowerCaseName()
console.log(myDatas);
}
}
function lowerCaseName(){
myDatas.name = myDatas.name.toLowerCase();
}
var decache = require('decache'); (1)
decache('./datas.json'); (2)
var myDatas = require('./datas.json');
module.exports = {
log: function() {
console.log(myDatas);
}
}
1 | First we need to require the decache module |
2 | Then we use decache to remove the datas.json file from the cache |
Maybe we should use decache from both module1.js and module2.js because if we invert the the two require lines in the index.js file the cache is not removed as we expect.
|
⇒ node index.js
{ version: 'O.1', name: 'myapplication' }
{ version: 'O.1', name: 'MyApplication' }
It works !
SUCCESS
The import Way (aka. the ES6 way) … FAIL
This idea was also given to me by Jérémy Morin.
As Node.JS (version 7.4.0 on my laptop) is not compatible with the ES6 (import) syntax, we will need to "babelize" (transpile with babel) the code, and for that we need to refactor our code, and initilaze a npm project.
-
First create a
src
doirectory and copy all files inside that directory -
In a terminal window run the
npm init
command and answer all questionnpm init
-
Then install the
babel-cli
dependencynpm install babel-cli --save-dev
-
Create a "build" script
package.json{ "name": "require_strange", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build": "babel src -d lib" (1) }, "author": "", "license": "ISC", "dependencies": {}, "devDependencies": { "babel-cli": "^6.24.1" } }
1 Now you can use npm run build
to build from ES6 code -
Configure babel :
-
Install the presets
npm install babel-preset-env --save-dev
-
Create the
.babelrc
/.babelrc{ "presets":["env"] }
-
-
Edit the Code to convert it to ES6
/src/datas.jsexport default { "version": "0.1", "name": "MyApplication" };
/src/index.jsimport module1 from './module1'; import module2 from './module2'; module1.log(); module2.log();
/src/module1.jsimport myDatas from './datas'; export default { log: function() { lowerCaseName(); console.log(myDatas); } }; function lowerCaseName() { myDatas.name = myDatas.name.toLowerCase(); }
/src/module2.jsimport myDatas from './datas'; export default { log: function() { console.log(myDatas); } }
-
Build
npm run build
-
Run
output 9⇒ node index.js { version: 'O.1', name: 'myapplication' } { version: 'O.1', name: 'myapplication' }
As you can see the execution output exactly the same, so this is not a good solution.
FAIL
Conclusion
As a conclusion, I would say that it is important to understand how the require
cache works, that’s why I wrote this article.
In order to solve the problem presented in the introduction, I would use the "clone" solution, by creating a "wrapper" object, maybe using the Proxy
from ES6.
Please feel free to make any comments …