Bye bye, singleton wiring and getInstance()!
CommonJS has a nice way of automatically creating singletons out of your modules. It's all in how you write your modules and what you export from them. No more hideous getInstance methods that drowns out the actual purpose of the module, sweet! If that's what you want, of course. If not, the behaviour might be a bit confusing... :)
CommonJS and Browserify
To be able to use the same syntax in your browser javascripts as you do in node, using CommonJS-modules with an exports-statement, you have to use a tool like Browserify or Webpack. I'm sure there are others out there too, but these are the ones I'm familiar with. Browserify is very easy to setup for small private projects. I'll try to walk you through it.
Project structure
The structure of this little project is the simplest possible: A source-folder containing Index.html and three js-files. Main.js is the entry point for the javascript source files and uses the colourFetcher.js and colourRepository.js to fetch and display colours. What we want to do is use Browserify to bundle up all the js-dependencies into one file that we can include in Index.html.
Install Browserify
You can find more info about Browserify on browserify.org. You need node on your machine, and then it's a cakewalk to install it:
In your root folder, create a package.json file if you don't already have one. This is done by running the command npm init in the terminal and answering the questions.
Install Browserify by running npm install browserify --save-dev in the terminal. This will install the package and add the dependency to the devDependencies-section of your package.json.
If you want to explore the options available, just type browserify in the terminal and take it from there. What we want to do now is just take all js-files in the src-folder and bundle them into a bundle.js-file placed in a public-folder. Just create a new folder named public under src and in the terminal, run browserify src/*.js -o src/public/bundle.js -d. The first part of the command is the glob-pattern for the files to bundle, the -o is the output location and -d stands for debug and means source maps will be generated. We probably don't want to have to remember this command, so change the script-section in package.json to look like this:
In your root folder, create a package.json file if you don't already have one. This is done by running the command npm init in the terminal and answering the questions.
Install Browserify by running npm install browserify --save-dev in the terminal. This will install the package and add the dependency to the devDependencies-section of your package.json.
If you want to explore the options available, just type browserify in the terminal and take it from there. What we want to do now is just take all js-files in the src-folder and bundle them into a bundle.js-file placed in a public-folder. Just create a new folder named public under src and in the terminal, run browserify src/*.js -o src/public/bundle.js -d. The first part of the command is the glob-pattern for the files to bundle, the -o is the output location and -d stands for debug and means source maps will be generated. We probably don't want to have to remember this command, so change the script-section in package.json to look like this:
"scripts": { "start": "browserify src/*.js -o src/public/bundle.js -d" }Now you can handily run the site with the command npm start instead and add the bundle.js as the js-source for your index.html.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Singleton</title> <script src="public/bundle.js"></script> </head> <body> </body> </html>
Add Watchify
To add a nice watch-function that immediately re-bundles your files when they're modified, install Watchify with npm install watchify. Modify your package.json again with:
"scripts": { "start": "browserify src/*.js -o src/public/bundle.js -d && watchify src/*.js -o src/public/bundle.js -d -v" }This will run browserify immediately followed by watchify with the verbose setting on (-v).
Back to the singleton issue
Now, we should have a nice environment up and running for trying out the quirks and wonders of CommonJS. For instance module caching. So let's create some code. First colourRepository.js:
Next up, colourFetcher.js, that requires colourRepository as a dependency and calls it:
So, what if we change the code in a couple of places? Instead of exporting a function in colourRepository.js, we change the last line to module.exports = colourRepository();. The exports-statement now returns the called function when the module is loaded. When we require the module in main.js and colourFetcher.js, we can now remove the call to that function: var colourRepo = require("./colourRepository");. As the code is run, the console only logs "new colourrepo" once, and with the tiniest of effort we've turned our colour repository into a singleton. :)
var colourRepository = function () { var colours = { magenta: "#FF00FF", palegreen: "#98FB98", chocolate: "#D2691E", }; var list = function (callback, message) { if (!message) message = "from repo"; callback(colours, message); }; console.log("new colourrepo"); return { list: list }; }; module.exports = colourRepository;ColourRepository.js just sets up a list of colours and passes them along into the callback function provided by the caller. If there's a message it gets sent back to the callback too, otherwise we add one.
Next up, colourFetcher.js, that requires colourRepository as a dependency and calls it:
var colourRepo = require("./colourRepository")(); var colourFetcher = function() { var list = function(callback) { colourRepo.list(callback, "from fetcher"); }; return { list: list }; }; module.exports = colourFetcher;And last, main.js, that requires both of the modules and therefore fetches colour in two different ways:
var colourRepo = require("./colourRepository")(); var colourFetcher = require("./colourFetcher")(); colourRepo.list(listColours); colourFetcher.list(listColours); function listColours(colours, message) { console.log(message); for (var colour in colours) { console.log(colours[colour] + '=' + colour); } }If we run this code, we notice that the console logs "new colourrepo" twice. Once when the repo is required in main.js and once when it's required from colourFetcher.js. This is because we're exporting colourRepository as a function. When we require it, we call the function at the same time using var colourRepo = require("./colourRepository")();. No caching, no singleton.
So, what if we change the code in a couple of places? Instead of exporting a function in colourRepository.js, we change the last line to module.exports = colourRepository();. The exports-statement now returns the called function when the module is loaded. When we require the module in main.js and colourFetcher.js, we can now remove the call to that function: var colourRepo = require("./colourRepository");. As the code is run, the console only logs "new colourrepo" once, and with the tiniest of effort we've turned our colour repository into a singleton. :)
Summary
Modules in CommonJS are cached after the first time they're loaded. This means that require("colourRepository") will return the same object everywhere, if it is resolved by the same file. If this is not the wanted behaviour, the exports-statement of the CommonJS-module should return a function instead, and the calling script must call that function.
Nice post! Stumbled upon this article on a related topic: http://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/. Not sure if the case sensitivity issue is present with Browserify, might be interesting to check out.
ReplyDelete