Skip to content

Commonjs 和 Es Module

一、模块化

html
<!-- 存在全局污染和依赖管理混乱问题 -->
<body>
  <script src="./index.js"></script>
  <script src="./home.js"></script>
  <script src="./list.js"></script>
</body
  • 全局污染

    ./home./list中分别定义function name() {}var name = '小明',在./list中添加打印name有可能输出一个函数

  • 依赖管理

    ./index中无法调用./home或者./list中的方法,只能下面的可以调用上面的。

二、Commonjs

1、实现原理

js
// hello.js
let name = '《JavaScript高级程序设计》'
module.exports = function sayName  (){
    return name
}
js
// home.js 实现hello.js的导入以及导出
const sayName = require('./hello.js')
module.exports = function say(){
    return {
        name:sayName(),
        author:'我不是外星人'
    }
}

存在moduleexportsrequire三个变量,然而这三个变量是没有被定义的,但是可以在Commonjs的规范下每一个js模块上直接使用他们。

在编译过程冲,实际Commonjs对js的代码进行了首尾包装:

js
(function(exports,require,module,__filename,__dirname){
   const sayName = require('./hello.js')
    module.exports = function say(){
        return {
            name:sayName(),
            author:'我不是外星人'
        }
    }
})

2、require文件加载流程

js
const fs =      require('fs')      // nodejs核心模块
const sayName = require('./hello.js')  // 路径形式的文件模块
const crypto =  require('crypto-js')   // 第三方自定义模块

核心模块的处理

核心模块的优先级仅次于缓存加载,在Node源码编译中,已被编译成二进制代码,因此加载过程中速度最快

路径形式的文件模块的处理

./../、和/开始的标识符,会被当做文件模块处理,require()方法会将路径转换成真实路径,并以真实路径作为索引,将编译后的结果缓存起来,第二次加载的时候会更快。

自定义模块的处理

查找规则:

  • 在当前目录下的node_mudules目录查找
  • 如果没有,在父级目录的node_modules查找,如果没有,就继续向父级目录查找node_modules
  • 向上递归查找,直到根目录下的node_modules
  • 在查找过程中,会找package,jsonmain属性指向的文件,如果没有package.json,在node环境会依次查找index.jsindex.jsonindex.node

3、require模块引入处理

Commonjs模块同步加载并执行模块文件,CommonJS模块在执行阶段分析模块依赖,采用深度优先遍历。

js
// a.js
console.log('a.js')
const getMes = require('./b')
console.log('我是 a 文件')
exports.say = function(){
    const message = getMes()
    console.log(message)
}
js
// b.js
console.log('b.js')
const say = require('./a')
const  object = {
    name:'《JavaScript高级程序设计》',
    author:'我不是外星人'
}
console.log('我是 b 文件')
module.exports = function(){
    return object
}
js
// main.js
const a = require('./a')
console.log('main.js')
const b = require('./b')

console.log('node 入口文件')

在终端输入 node main.js,打印如下

shell
PS D:\Code\Source\common-question> node ./cjs-es/main.js
a.js
b.js
我是 b 文件
我是 a 文件
main.js
node 入口文件
PS D:\Code\Source\common-question>

根据以上运行结果得出结论:

  • main.jsa.js模块都引用了b.js模块,但是b.js模块只执行了一次
  • a.js模块和b.js模块相互引用,但是没有造成循环引用的情况
  • 主席那个顺序是父 --> 子 --> 父

3.1、require加载原理

module:在Node中每一个js文件都是一个module,module上保存了exports等信息外,还有一个loaded表示该模块是否被加载。true表示已经加载,false表示还未加载。

Module:以nodejs为例,整个系统运行之后,会用Module缓存每一个模块加载的信息。

require源码大致如下:

js
 // id 为路径标识符
function require(id) {
   /* 查找  Module 上有没有已经加载的 js  对象*/
   // require 会接收一个参数——文件标识符,然后分析定位文件,
   // 分析过程我们上述已经讲到了,加下来会从 Module 上查找有没有缓存,
   // 如果有缓存,那么直接返回缓存的内容
   const  cachedModule = Module._cache[id]
   
   /* 如果已经加载了那么直接取走缓存的 exports 对象  */
  if(cachedModule){
    return cachedModule.exports
  }
 
  /* 创建当前模块的 module  */
  // 如果没有缓存,会创建一个 module 对象,缓存到 Module 上,然后执行文件,
  // 加载完文件,将 loaded 属性设置为 true ,然后返回 module.exports 对象。
  // 借此完成模块加载流程
  const module = { exports: {} ,loaded: false , ...}

  /* 将 module 缓存到  Module 的缓存属性中,路径标识符作为 id */  
  Module._cache[id] = module
  /* 加载文件 */
  runInThisContext(wrapper('module.exports = "123"'))(module.exports, require, module, __filename, __dirname)
  /* 加载完成 *//
  module.loaded = true 
  /* 返回值 */
  // 模块导出就是 return 这个变量的其实跟 a = b 赋值一样, 
  // 基本类型导出的是值, 引用类型导出的是引用地址
  return module.exports
}
// exports 和 module.exports 持有相同引用,因为最后导出的是 module.exports,
// 所以对 exports 进行赋值会导致 exports 操作的不再是 module.exports 的引用

3.2、require避免重复加载

从上面我们可以直接得出,require 如何避免重复加载的,首先加载之后的文件的 module 会被缓存到 Module 上,比如一个模块已经 require 引入了 a 模块,如果另外一个模块再次引用 a ,那么会直接读取缓存值 module ,所以无需再次执行模块。

3.3、require避免循环引用

① 首先执行 node main.js ,那么开始执行第一行 require(a.js)

② 那么首先判断 a.js 有没有缓存,因为没有缓存,先加入缓存,然后执行文件 a.js (需要注意 是先加入缓存, 后执行模块内容);

③ a.js 中执行第一行,引用 b.js。

④ 那么判断 b.js 有没有缓存,因为没有缓存,所以加入缓存,然后执行 b.js 文件。

⑤ b.js 执行第一行,再一次循环引用 require(a.js) 此时的 a.js 已经加入缓存,直接读取值。接下来打印 console.log('我是 b 文件'),导出方法。

⑥ b.js 执行完毕,回到 a.js 文件,打印 console.log('我是 a 文件'),导出方法。

⑦ 最后回到 main.js,打印 console.log('node 入口文件') 完成这个流程。

4、require动态加载

js
// a.js
console.log('我是 a 文件')
exports.say = function(){
    const getMes = require('./b') // 动态加载b.js模块,require本质是一个函数
    const message = getMes()
    console.log(message)
}
js
// main.js
const a = require('./a')
a.say()
shell
PS D:\Code\Source\common-question> node ./cjs-es/main.js
我是 a 文件
b.js
我是 b 文件
{ name: '《JavaScript高级程序设计》', author: '我不是外星人' }
PS D:\Code\Source\common-question>

5、exports和module.exports

js
exports.name = 'alien' // 此时 exports.name 是无效的
module.exports ={
    name:'《React进阶实践指南》',
    author:'我不是外星人',
    say(){
        console.log(666)
    }
}
  • 上述情况下 exports.name 无效,会被 module.exports 覆盖。

三、ES Module

1、导出export和导入import

1.1、export正常导出和import

js
// 导出模块:a.js
const name = '《JavaScript高级程序设计》' 
const author = '我不是外星人'
export { name, author }
export const say = function (){
    console.log('hello , world')
}
js
// 导入模块:main.js
// name , author , say 对应 a.js 中的  name , author , say
import { name , author , say } from './a.js'
  • export {},与变量名绑定,命名导出
  • import {} from 'module',导入modeul的命名导出
  • import {}内部的变量名要与export{}完全匹配

1.2、默认导出export default

js
// 导出模块:a.js
const name = '《JavaScript高级程序设计》'
const author = '我不是外星人'
const say = function (){
    console.log('hello , world')
}
export default {
    name,
    author,
    say
}
js
// 导入模块:main.js
import mes from './a.js'
console.log(mes) //{ name: '《JavaScript高级程序设计》',author:'我不是外星人', say:Function }
  • export default anything 导入 module 的默认导出。 anything 可以是函数,属性方法,或者对象。
  • 对于引入默认导出的模块,import anyName from 'module', anyName 可以是自定义名称。

1.3、混合导入导出

js
// 导入模块:a.js
export const name = '《JavaScript高级程序设计》'
export const author = '我不是外星人'

export default  function say (){
    console.log('hello , world')
}
js
// 导入模块:main.js   第一种导入方式
import theSay , { name, author as  bookAuthor } from './a.js'
console.log(
    theSay,     // ƒ say() {console.log('hello , world') }
    name,       // "《JavaScript高级程序设计》"
    bookAuthor  // "我不是外星人"
)
js
// 导入模块:main.js   第二种导入方式
import theSay, * as mes from './a'
console.log(
    theSay, // ƒ say() { console.log('hello , world') }
    mes // { name:'《JavaScript高级程序设计》' , author: "我不是外星人" ,default:  ƒ say() { console.log('hello , world') } }
)

1.4、重属名导入

js
import {  name as bookName , say,  author as bookAuthor  } from 'module'
console.log( bookName , bookAuthor , say ) //《React进阶实践指南》 我不是外星人

1.5、重定向导出

可以把当前模块作为一个中转站,一方面引入 module 内的属性,然后把属性再给导出去。

js
export * from 'module' // 第一种方式
export { name, author, ..., say } from 'module' // 第二种方式
export {   name as bookName ,  author as bookAuthor , ..., say } from 'module' //第三种方式
  • 第一种方式:重定向导出 module 中的所有导出属性, 但是不包括 module 内的 default 属性。
  • 第二种方式:从 module 中导入 name ,author ,say 再以相同的属性名,导出。
  • 第三种方式:从 module 中导入 name ,重属名为 bookName 导出,从 module 中导入 author ,重属名为 bookAuthor 导出,正常导出 say 。

1.6、无需导入模块,只运行模块

js
import 'module'

1.7、动态导入

js
const promise = import('module')

2、ES Module特性

2.1、静态语法

ES Module的导入和导出是境内太的,import会自动提升到代码的顶层,importexport不能放在块级作用域或条件语句中。

js
// 错误写法一
function say(){
  import name from './a.js'  
  export const author = '我不是外星人'
}

// 错误写法二
isexport &&  export const  name = '《React进阶实践指南》'

// 错误写法三
let name = 'Export'
import 'default' + name from 'module'

2.2、执行特性

CommonJS模块同步加载并执行模块文件,ES Module模块提前加载并执行模块文件。

ES Module在预处理阶段分析模块,在执行阶段执行模块,两个阶段都采用深度优先遍历,执行顺序是子 --> 父

js
// main.js
console.log('main.js开始执行')
import say from './a'
import say1 from './b'
console.log('main.js执行完毕')
js
// a.js
import b from './b'
console.log('a模块加载')
export default  function say (){
    console.log('hello , world')
}
js
// b.js
console.log('b模块加载')
export default function sayhello(){
    console.log('hello,world')
}

运行顺序如下

txt
b模块加载
a模块加载
main.js开始执行
main.js执行完毕

2.3、导出绑定

使用 import 被导入的模块运行在严格模式下。

使用 import 被导入的变量是只读的,可以理解默认为 const 装饰,无法被赋值

使用 import 被导入的变量是与原变量绑定/引用的,可以理解为 import 导入的变量无论是否为基本类型都是引用传递。

3、import()动态导入

import()可以动态使用,import()返回一个Promise对象,返回的Promisethen成功回调中,可以获取模块加载成功信息。

vue
<script>
export default defineComponent({
  name: 'CustomComponent',
  props: {
    step: {
      type: Object,
      default: () => ({}),
    },
  },
  setup(props) {
    const customRes = computed(() => {
        return () => obj.res; // { res: import(./name.vue) }
    });
    return {
      customRes,
    };
  },
  render(h) {
    return (
      <div class="layout">
          {h(this.customRes, {
            props: {
              step: this.step,
            },
          })}
      </div>
    );
  },
});
</script>
<style lang="scss">

</style>

4、CommonJS和ES Module

4.1、commonjs

  • CommonJS模块由JS运行时实现
  • CommonJS是单个值导出,本质上导出的是exports属性
  • CommonJS可以动态加载,对每一个加载都存在缓存,可以有效的解决循环引用的问题
  • CommonJS模块同步加载并执行模块文件

4.2、es module

  • ES Module是静态的,不能放在块级作用域内,代码发生在编译时
  • ES Module的值是动态绑定的,可以通过导出方法修改,可以直接访问修改结果
  • ES Module可以导出多个属性和方法,可以单个导入导出,混合导入导出
  • ES Module提前(编译时)加载模块并执行模块文件,容易实现Tree Shaking 和 Code Splitting

四、其他模块规范

CommonJS

Node.js的模块化规范,通过require引入模块,通过module.exports导出模块。(2009)

  • 一个单独文件,就是一个模块

  • 通过require引入模块,通过module.exports导出模块。

  • 模块是同步加载的,适用于服务器端。(运行时加载)

  • 模块加载时,会缓存模块的导出结果,再次加载时直接返回缓存结果。

  • 模块导出的是值的浅拷贝,模块内部修改导出的值不会影响外部。

    js
    // math.js
    function add(a, b) {
      return a + b;
    }
    module.exports = {
      add: add
    }
    
    // main.js
    const math = require('./math.js');
    console.log(math.add(1, 2)); // 3

AMD

RequireJS实现的模块化规范,通过define定义模块,通过require引入模块。(2010)

  • 通过define定义模块,通过require引入模块。

  • 模块是异步加载的,适用于浏览器端

  • AMD兼容CommonJS规范。

  • 延迟加载,按需加载。

js
// 定义  有依赖模块
// a.js
define([], function() {
  const message = "Hello from Module 1";
  return {
    message,
  };
})

// 引入 a.js
require(['./a.js'], function(a) {
  console.log(a.message); // Hello from Module 1
})

CMD

SeaJS实现的模块化规范,通过define定义模块,通过require引入模块。(2011)

  • 通过define定义模块,通过require引入模块。

  • 模块是异步加载的,适用于浏览器端。

  • CMD兼容CommonJS规范。

  • 依赖就近

  • 加载方式:seajs.use('./app.js')

    js
    // a.js
    define(function(require, exports, module) {
      const message = "Hello from Module 1";
      module.exports = {
        message
      };
    });
    
    // app.js
    define(function(require) {
      const a = require('./a');
      console.log(a.message);
      return {
          name: '小易'
      }
    });
    html
    <!-- // index.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>CMD Example</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/seajs/3.0.0/sea.js"></script>
    </head>
    <body>
        <h1>Check the console for output</h1>
        <script>
            seajs.use('./app.js', app => {
                console.log(app.name) // 小易
            });
        </script>
    </body>
    </html>

UMD

通用模块定义规范,兼容CommonJS、AMD和全局变量方式。(2012)

  • 模块是异步加载的。

  • UMD兼容CommonJSAMD规范,都不满足 => 全局变量方式

  • 适用于浏览器和Node.js环境

    js
    (function (root, factory) {
        if (typeof define === 'function' && define.amd) {
            // AMD 模块系统 (RequireJS)
            define(factory);
        } else if (typeof module === 'object' && module.exports) {
            // CommonJS 模块系统 (Node.js)
            module.exports = factory();
        } else {
            // 作为全局变量暴露
            root.myModule = factory();
        }
    }(this, function () {
      // 模块代码
      const message = "Hello, UMD!";
      return {
        getMessage: function() {
          return message;
        }
      };
    }));

ES6 Module:

ES6引入的模块化规范,通过import引入模块,通过export导出模块。(2015)

  • 通过export导出模块,通过import引入模块。

  • 模块是静态加载的,在编译时确定模块的依赖关系。

  • 适用于浏览器和Node.js环境(v6.10.3 版本就开始支持)。

  • 不同于CommenJS,ES6 Module输出的是值的引用,而不是浅复制。

  • ES6 Module是官方提供的模块化方案,,其它都是社区实现的。(ES2015)

  • 指定加载某个输出值,而不是整个模块,有利于代码分割tree shaking

  • import 导入的的变量存在 声明提升