With the development of componentized front-end framework and front-end routing technology, modularization has become a necessary skill for modern front-end engineers. No matter what language is developed to a certain extent, its engineering capability and maintainability is bound to get corresponding development.

Modularity is a fairly common thing in any programming domain, and the point of modularity is to increase reusability, to achieve personalized needs with as little code as possible. CSS, one of the three front-end players, proposed @import as early as in version 2.1 to achieve modularity, but JavaScript did not have an official modularity solution until ES6: ES Module (import, export). Although the early JavaScript language specifications did not support modularity, this did not stop the development of JavaScript, and there was no official modularity standard for developers to create their own specifications and implement their own specifications.

The emergence of the CommonJS

Ten years ago the front end was not as hot as it is now, and modularity was simply implementing a namespace using closures. 2009 is definitely a big year for JavaScript, with a new JavaScript engine (V8), mature libraries (jQuery, YUI, Dojo), and ES5 being proposed, JavaScript is still only available in browsers. Back in 2007, AppJet offered a service to create and host server-side JavaScript applications. Later Aptana also provided an environment for running Javascript on the server, called Jaxer. There are blogs about AppJet, Jaxer, and even the Jaxer project on Github.

But none of these things took off, and Javascript is no substitute for traditional server-side scripting languages (PHP, Python, Ruby). Although it has many disadvantages, it does not prevent many people from using it. And then people started thinking, what else do you need for JavaScript to run on the server side? So in January 2009, Mozilla engineer Kevin Dangoor launched CommonJS, a proposal for JavaScript enthusiasts to get together and write specifications for JavaScript to run on the server side. A week later, That’s 224 participants.

“[This] is not a technical problem,It’s a matter of people getting together and making a decision to step forward and start building up something bigger and cooler together.”

The CommonJS standard covers the basic capabilities that JavaScript needs to run on the server side, such as modularity, IO manipulation, binary strings, process management, and Web Gateway Interface (JSGI). But the most far-reaching is CommonJS’s modularity approach, which was the JavaScript community’s first effort at a module system that supported not only dependency management but also scope isolation and module identification. When Node.js came along, he directly adopted the CommonJS modularity specification, along with NPM (Node Package Manager, now the largest module repository in the world).

CommonJS works so well on the server that a lot of people want to port it to the client (aka the browser). As a result of the CommonJS module loading is synchronous, and the service side read directly from the disk or in memory, time can be largely ignored, but if in the browser or synchronous loading, is extremely hostile to the user experience, module loading process will inevitably request other modules of code to the server, network request process can cause bad for a long time. So from the CommonJS gradually split out some factions, in the process of the development of these factions, there have been some familiar industry plan AMD, CMD, packaging tools (Component/Browserify/Webpack).

AMD specification: RequireJS

RequireJS is the representative work of AMD specification. The reason why it can represent AMD specification is that James Burke, the author of RequireJS, was the author of AMD specification. The author also developed Amdefine, a library that allows you to use AMD specifications in Node.

The AMD specification grew out of CommonJS ‘Modules/Transport/C proposal, which was undoubtedly initiated by James Burke.

James Burke points out some shortcomings of the CommonJS specification in browsers:

  1. Lack of module encapsulation: Every module in the CommonJS specification is a file. This means there is only one module per file. This works on the server, but it’s not so friendly in the browser, where you need to make as few requests as possible.
  2. Use synchronous loading of dependencies: While synchronous loading makes the code easier to understand, using synchronous loading in the browser can lead to a long blank screen that affects the user experience.
  3. The CommonJS specification uses a name calledexportObject to expose the module that will need to export variables to attachexportBut you cannot assign to the object directly. If you need to export a constructor, use itmodule.export“It can be confusing.

The AMD specification defines a define global method for defining and loading modules, and RequireJS later extended the Require global method for loading modules. This method solves the shortcoming of using CommonJS specification in browser.

define(id? , dependencies? , factory);Copy the code
  1. Using anonymous functions to encapsulate modules and define modules by function return values is more syntactic in JavaScript, avoiding the dependency of exports variables and the problem that a file can only expose one module.

  2. Listing dependencies ahead of time and loading them asynchronously, as in the browser, allows modules to use them out of the box.

    define("foo"["logger"].function (logger) {
        logger.debug("starting foo's definition")
        return {
            name: "foo"}})Copy the code
  3. Give the module a module ID (name) that uniquely identifies the module in the definition. In addition, AMD’s module name specification is a superset of the CommonJS module name specification.

    define("foo".function () {
        return {
            name: 'foo'}})Copy the code

RequireJS principle

Before we talk about how it works, we can take a look at the basic uses of RequireJS.

  • Module information configuration:

    require.config({
      paths: {
        jquery: 'https://code.jquery.com/jquery-3.4.1.js'}})Copy the code
  • Dependency module loading and calling:

    require(['jquery'].function ($){$('#app').html('loaded')})Copy the code
  • Module definition:

    if ( typeof define === "function" && define.amd ) {
      define( "jquery"[],function() {
        returnjQuery; }); }Copy the code

We first used the config method to configure the path of the jquery module, then called the require method to load the jquery module, and then called the loaded $object in the callback. In this process, jquery exposes the $object we need using the define method.

With a basic understanding of the process, we moved on to the principles of RequireJS.

Module Information Configuration

Module information configuration, in fact, is very simple, only a few lines of code can be implemented. Define a global Object, then extend the Object using object.assign.

// Configuration information
const cfg = { paths: {}}// the global require method
req = require = (a)= > {}

// Extend the configuration
req.config = config= > {
  Object.assign(cfg, config)
}
Copy the code

Dependency module load and call

The logic of the require method is very simple. After a simple parameter verification, the Module is instantiated by calling getModule. GetModule will cache the instantiated Module. Because the require method does not have a module name when it executes a module instance, it produces an anonymous module. Module class, we can understand as a Module loader, the main function is to load the dependency, and after the dependency loading, call the callback function, and pass the dependent modules as parameters back to the callback function.

// the global require method
req = require = (deps, callback) = > {
  if(! deps && ! callback) {return
  }
  if(! deps) { deps = [] }if (typeof deps === 'function') {
    callback = deps
    deps = []
  }
  const mod = getModule()
  mod.init(deps, callback)
}

let reqCounter = 0
const registry = {} // Registered module

// Factory method of the module loader
const getModule = name= > {
  if(! name) {// If the module name does not exist, it is represented as an anonymous module, and the module name is automatically constructed
    name = `@mod_${++reqCounter}`
  }
  let mod = registry[name]
  if(! mod) { mod = registry[name] =new Module(name)
  }
  return mod
}
Copy the code

Module loader is the core of the whole module loading, mainly including enable method and check method.

After the module loader completes the instantiation, it first calls init, passing in the module’s dependencies and callbacks.

// Module loader

class Module {
  constructor(name) {
    this.name = name
    this.depCount = 0
    this.depMaps = []
    this.depExports = []
    this.definedFn = (a)= > {}
  }
  init(deps, callback) {
    this.deps = deps
    this.callback = callback
    // Determine whether a dependency exists
    if (deps.length === 0) {
      this.check()
    } else {
      this.enable()
    }
  }
}
Copy the code

The enable method is mainly used for module dependency loading. The main logic of this method is as follows:

  1. Iterate through all dependent modules;

  2. Record the number of loaded modules (this.depcount ++). This variable is used to determine whether all dependent modules are loaded.

  3. Instantiate the module loader that depends on the module and bind the definedFn method;

    The definedFn method is called after the dependency module is loaded. Its main function is to get the contents of the dependency module, subtract depCount by 1, and finally call check (this method determines if depCount is less than 1 to determine that the dependency is loaded completely).

  4. Finally, obtain the path of the dependent module from the configuration based on the name of the dependent module to load the module.

class Module {...// Enable the module for dependency loading
  enable() {
    // Iterate over dependencies
    this.deps.forEach((name, i) = > {
      // Record the number of modules loaded
      this.depCount++
      
      // instantiate the module loader that depends on the module and bind the module callback when it has loaded
      const mod = getModule(name)
      mod.definedFn = exports= > {
        this.depCount--
        this.depExports[i] = exports
        this.check()
      }
      
      // Obtain the path of the dependent module in the configuration to load the module
      consturl = cfg.paths[name] loadModule(name, url) }); }... }Copy the code

The main function of loadModule is to load a JS file via url and bind an onload event. Onload retrieves the module loader that the dependent module has instantiated and calls the init method.

// Cache loaded modules
const defMap = {}

// Dependency loading
const loadModule =  (name, url) = > {
  const head = document.getElementsByTagName('head') [0]
  const node = document.createElement('script')
  node.type = 'text/javascript'
  node.async = true
  // Set a data attribute to get the module name after the dependency is loaded
  node.setAttribute('data-module', name)
  node.addEventListener('load', onScriptLoad, false)
  node.src = url
  head.appendChild(node)
  return node
}

// Node binding onload event function
const onScriptLoad = evt= > {
  const node = evt.currentTarget
  node.removeEventListener('load', onScriptLoad, false)
  // Get the module name
  const name = node.getAttribute('data-module')
  const mod = getModule(name)
  const def = defMap[name]
  mod.init(def.deps, def.callback)
}
Copy the code

Seeing from the previous example, because there is only one dependency (jQuery) and the jQuery module has no other dependencies, the init method calls the check method directly. You can also think about, if it’s a module with dependencies what’s the next flow?

define( "jquery"And []/* No other dependencies */.function() {
  returnjQuery; });Copy the code

The check method is mainly used to detect dependencies and invoke callbacks after dependencies have been loaded.

// Module loader
class Module {...// Check whether dependencies are loaded
  check() {
    let exports = this.exports
    // If the dependency number is less than 1, all dependencies are loaded
    if (this.depCount < 1) { 
      // Invoke the callback and get the contents of the module
      exports = this.callback.apply(null.this.depExports)
      this.exports = exports
      // Activate the defined callback
      this.definedFn(exports)
    }
  }
  ...
}
Copy the code

Finally go back to the dependent module via definedFn, that is, the anonymous module loader instantiated by the require method originally, store the contents exposed by the dependent module in depExports, then call the check method of the anonymous module loader and call the callback.

mod.definedFn = exports= > {
  this.depCount--
  this.depExports[i] = exports
  this.check()
}
Copy the code

The module definition

There is also a question is, in the dependency module loaded callback, how to get the dependency module dependency and callback?

const def = defMap[name]
mod.init(def.deps, def.callback)
Copy the code

The answer is through the globally defined define method, which stores module dependencies and callbacks into a global variable that can then be retrieved as needed.

const defMap = {} // Cache loaded modules
define = (name, deps, callback) = > {
  defMap[name] = { name, deps, callback }
}
Copy the code

Summary of RequireJS principle

Finally, it can be found that the core of RequireJS lies in the implementation of module loader, which is inseparable from module loader either through require or define module.

For those interested, check out the full code for the simplified Version of RequrieJS on github.

CMD specification: sea-.js

CMD specification by the domestic developer Yu Bo put forward, although in the international visibility is far less than AMD, but in the domestic and AMD also go hand in hand. CMD is more lazy than AMD’s asynchronous loading, and the specification of CMD is closer to CommonJS, just adding a function call wrapper around CommonJS.

define(function(require, exports, module) {
  require("./a").doSomething()
  require("./b").doSomething()
})
Copy the code

The implementation of the CMD specification, Sea-.js, also implements an API similar to RequireJS:

seajs.use('main'.function (main) {
  main.doSomething()
})
Copy the code

Sea-.js is the same as RequireJS in module loading, which is loaded by inserting script tag into head tag, but there are certain differences in loading sequence. To illustrate the difference between the two, let’s go straight to code:

RequireJS :

// RequireJS
define('a'.function () {
  console.log('a load')
  return {
    run: function () { console.log('a run') }
  }
})

define('b'.function () {
  console.log('b load')
  return {
    run: function () { console.log('b run')}}})require(['a'.'b'].function (a, b) {
  console.log('main run')
  a.run()
  b.run()
})
Copy the code

sea.js :

// sea.js
define('a'.function (require, exports, module) {
  console.log('a load')
  exports.run = function () { console.log('a run') }
})

define('b'.function (require, exports, module) {
  console.log('b load')
  exports.run = function () { console.log('b run') }
})

define('main'.function (require, exports, module) {
  console.log('main run')
  var a = require('a')
  a.run()
  var b = require('b')
  b.run()
})

seajs.use('main')
Copy the code

As you can see, the sea-js module is lazily loaded, and only when required will the module actually be run. RequireJS, on the other hand, runs all dependencies first, gets the results of all dependencies exposed, and then executes the callback.

Because of the lazy loading mechanism, sea.js provides seajs.use methods to run defined modules. All callbacks to define are not executed immediately. Instead, all callbacks to define are cached. Only callbacks to use and required modules are executed.

Sea. Js principle

The following is a brief explanation of the lazy loading logic of sea-.js. When the define method is called, it simply puts the module into a global object for caching.

const seajs = {}
const cache = seajs.cache = {}

define = (id, factory) = > {
  const uri = id2uri(id)
  const deps = parseDependencies(factory.toString())
  const mod = cache[uri] || (cache[uri] = new Module(uri))
  mod.deps = deps
  mod.factory = factory
  
}

class Module {
  constructor(uri, deps) {
    this.status = 0
    this.uri    = uri
    this.deps   = deps
  }
}
Copy the code

Here, Module is a Module loader similar to RequireJS. Seajs.use, which follows, takes the corresponding module from the cache and loads it.

Note: This section of code is only a brief introduction to the logic of the use method and does not run directly.

let cid = 0
seajs.use = (ids, callback) = > {
  const deps = isArray(ids) ? ids : [ids]
  
  deps.forEach(async (dep, i) => {
    const mod = cache[dep]
    mod.load()
  })
}
Copy the code

In addition, sea-js dependencies are declared in factory. When the module is called, sea-js will convert factory into a string, and then match all the XXX in require(‘ XXX ‘) to store the dependency. The parseDependencies method in the previous code does just that.

In the early days, sea-js was matched directly by means of re:

const parseDependencies = (code) = > {
  const REQUIRE_RE = (/ "? : \ \ | "[^"]) * '|' (? :\\'|[^'])*'|\/\*[\S\s]*? \ * \ | \ / (? :\\\/|[^/\r\n])+\/(? =[^\/])|\/\/.*|\.\s*require|(? :^|[^$])\brequire\s*\(\s*(["'])(.+?) \1\s*\)/g
  const SLASH_RE = /\\\\/g
  const ret = []

  code
    .replace(SLASH_RE, ' ')
    .replace(REQUIRE_RE, function(_, __, id) {
      if (id) {
        ret.push(id)
      }
    })
  return ret
}
Copy the code

However, it was later found that the regex was prone to various bugs, and the excessively long regex was not conducive to maintenance. Therefore, Sea-js abandoned this method in the later period, and used the state machine for lexical analysis to obtain require dependency.

For detailed code, see the sea-.js subproject: Crequire.

Summary of principle of sea-.js

In fact, the code logic of Sea-.js is basically similar to RequireJS, which is to load modules by creating script tags and implement a module recorder for managing dependencies.

The main difference lies in the lazy loading mechanism of Sea-.js, and in the way of use, all dependencies of Sea-.js are not declared in advance, but are extracted manually by means of regular or lexical analysis inside Sea-.js.

For those interested, see the full code for the simplified version of Sea-.js on my Github.

conclusion

ES6’s modularity specification is getting better and better, and its statically minded approach facilitates later packaging tools and supports Tree Shaking in a friendly way. It may seem boring to learn about these outdated modular solutions, but history can’t be forgotten, and we should learn more about the context in which these things emerged and how they were solved, rather than complaining about how quickly new things change.

Enough chicken soup, dig a hole, and stay tuned for the next issue of The Life of Front-end modularity.