Node.js与CommonJS的关系

126 阅读6分钟

一、前端模块化规范

前端需要模块化,并且模块化不光要处理全局变量污染数据保护的问题,还要很好的解决模块之间依赖关系的维护。

既然JavaScript 需要模块化来解决上面的问题,那就需要制定模块化的规范,CommonJS 就是解决上面问题的模块化规范之一

参考链接—— 为什么需要CommonJS

1. CommonJS

为什么要学习CommonJS? 因为node.js最开始是参照CommonJS这个模块化规范做起来的,所以要了解CommonJS他到底为了实现模块化制定了什么规范。

1.1 require

require 用来加载某个模块,module 代表当前模块,是一个对象,保存了当前模块的信息。exports 是 module 上的一个属性,保存了当前模块要导出的接口或者变量,使用 require 加载的某个模块获取到的值就是那个模块使用 exports 导出的值。

require 命令的基本功能是,读入并执行一个 js 文件,然后返回该模块的 exports 对象。如果没有发现指定模块,会报错。

第一次加载某个模块时,Node.js 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的 module.exports 属性返回了。
注意:CommonJS 模块的加载机制是,require 的是被导出的值的拷贝。也就是说,一旦导出一个值,模块内部的变化就影响不到这个值 。

// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.age = age
exports.setAge = function(a){
    age = a
}
// b.js
var a = require('a.js')
console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 18

1.2 CommonJS实现

了解 CommonJS 的规范后,不难发现我们在写符合 CommonJS 规范的模块时,无外乎就是使用了 require 、 exports 、 module 三个东西,然后一个 js 文件就是一个模块。

如果我们向一个 立即执行函数 提供 require 、 exports 、 module 三个参数,模块代码放在这个立即执行函数里面。模块的导出值放在 module.exports 中,这样就实现了模块的加载。如下所示:

JS立即执行函数模式是一种语法,可以让你的函数在定义后立即被执行,这种模式本质上就是函数表达式(命名的或者匿名的),在创建后立即执行。

注意: 1. 函数体后面要有小括号 2. 函数体必须是函数表达式而不能是函数声明

深入理解JavaScript——立即执行函数(IIFE) - 掘金 (juejin.cn)

(function(name){
})(name)   //其实就是将外面括号中的name传给function来使用
(function(module, exports, require) {
    // b.js
    var a = require("a.js")
    console.log('a.name=', a.name)
    console.log('a.age=', a.getAge())

    var name = 'lilei'
    var age = 15
    exports.name = name
    exports.getAge = function () {
      return age
    }

})(module, module.exports, require)  //将外面括号中的module,module.exports, require作为实参传入函数

许多打包工具都是通过把所依赖的模块放到各自的函数中,将所有模块打包成一个能在浏览器中运行的js文件,以实现将符合CommonJS模块规范的项目代码,转换为浏览器支持的代码,比如webpack。

以下代码展示了webpack是如何在打包模块时实现对CommonJS规范的支持。

  • bundle.js是一个立即执行的匿名函数,所以能在浏览器中直接运行。函数体内包含了模块管理的具体内容
  • 匿名函数后面包含了多个js文件的小括号,将这些文件内容传入匿名函数中
// bundle.js
(function (modules) {
  // 模块管理的实现
  var installedModules = {}
  /**
   * 加载模块的业务逻辑实现
   * @param {String} moduleName 要加载的模块名
   */
  var require = function (moduleName) {

    // 如果已经加载过,就直接返回
    if (installedModules[moduleName]) return installedModules[moduleName].exports

    // 如果没有加载,就生成一个 module,并放到 installedModules
    var module = installedModules[moduleName] = {
      moduleName: moduleName,
      exports: {}
    }

    // 执行要加载的模块
    // call函数第一个参数表示实际要执行的对象,后面为传入modules[moduleName]中的参数
    // 可以理解为 'a.js'.call(installedModules['a.js'] ,module, module.exports, require)
    modules[moduleName].call(module.exports, module, module.exports, require)

    return module.exports
  }

  return require('index.js')
})({
  'a.js': function (module, exports, require) {
    // a.js 文件内容
  },
  'b.js': function (module, exports, require) {
    // b.js 文件内容
  },
  'index.js': function (module, exports, require) {
    // index.js 文件内容
  }
})

2、其他前端模块化

由于CommJS的 require 命令是执行完一个文件后返回他的exports对象,这在服务器端是可行的,因为服务端加载并执行一个文件的时间消费是可以忽略的,模块的加载是运行时同步加载的,require 命令执行完后,文件就执行完了,并且成功拿到了模块导出的值。

但这不适合浏览器端,因为它是同步的。可想而知,浏览器端每加载一个文件,要发网络请求去取,如果网速慢,就非常耗时,浏览器就要一直等 require 返回,就会一直卡在那里,阻塞后面代码的执行,从而阻塞页面渲染,使得页面出现假死状态。

为了解决以上的问题,出现了众多前端模块化规范,如下图所示 image.png

2.1 AMD和CMD

AMD和CMD都是用在浏览器端的模块规范

  • AMD使用 define 以及define中的参数来进行模块化,采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在回调函数中,等到加载完成之后,这个回调函数才会运行。
//AMD示例
// a.js
define(function(){
    var name = 'morrain'
    var age = 18
    return {
        name,
        getAge: () => age
    }
})
// b.js
define(['a.js'], function(a){
    var name = 'lilei'
    var age = 15
    console.log(a.name) // 'morrain'
    console.log(a.getAge()) // 18
    return {
        name,
        getAge: () => age
    }
})
  • CMD采用同步的形式书写模块代码,首先要在 html 文件中引入 sea.js 工具库,就是这个库提供了定义模块、加载模块等功能。它提供了一个全局的 define 函数用来定义模块。当 b.js 模块被 require 时,b.js 加载后,Sea.js 会扫描 b.js 的代码,找到 require 这个关键字,提取所有的依赖项,然后加载,等到依赖的所有模块加载完成后,执行回调函数,此时再执行到 require('a.js') 这行代码时,a.js 已经加载好在内存中了
// 所有模块都通过 define 来定义
define(function(require, exports, module) {

  // 通过 require 引入依赖
  var a = require('xxx')
  var b = require('yyy')

  // 通过 exports 对外提供接口
  exports.doSomething = ...

  // 或者通过 module.exports 提供整个接口
  module.exports = ...

})

// CMD示例
// a.js
define(function(require, exports, module){
    var name = 'morrain'
    var age = 18

    exports.name = name
    exports.getAge = () => age
})
// b.js
define(function(require, exports, module){
    var name = 'lilei'
    var age = 15
    var a = require('a.js')

    console.log(a.name) // 'morrain'
    console.log(a.getAge()) //18

    exports.name = name
    exports.getAge = () => age
})

2.2 ES6 Module

ES6 Module 是 ES6 中对模块的规范,ES6 是 ECMAScript 6.0 的简称,是 JavaScript 语言的下一代标准,在 2015 年 6 月正式发布,Node.js已经开始支持ES6的各种用法。

CommonJS 是服务于服务端的,而 AMD、CMD 是服务于浏览器端的,但它们都有一个共同点:都在代码运行后才能确定导出的内容, 而 ES6 Module 的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及导入和导出的变量,也就是所谓的 编译时加载

正因为如此,import 命令具有提升效果,会提升到整个模块的头部,首先执行。也正因为 ES6 Module 是编译时加载, 所以不能使用表达式和变量,因为这些是只有在运行时才能得到结果的语法结构。

ES6与CommonJS不同,传输的不是拷贝的值,因此以下代码会有不同的效果

// a.js
var name = 'morrain'
var age = 18
const setAge = a => age = a
export {
    name,
    age,
    setAge
}

// b.js
import * as a from 'a.js'

console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 19

二、Node.js

参考内容——《深入浅出Node.js》朴灵