前端工程化之模块化基础

615 阅读18分钟

什么是模块化?

模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。模块化可以让我们将代码分功能和业务等维度来切分成小的组织单元,然后根据需要来更好的组织代码,增加代码的复用性、可维护性和扩展性。

模块化带来了以下几个优点:

  • 代码抽象:将可复用的代码逻辑抽象到通用的库,屏蔽实现的复杂性。
  • 代码封装:将代码的实现封装到模块,对外暴露API,从而使调用模块不必关注实现细节。
  • 代码复用:也就是 DRYDon't Repeat Yourself),减少代码的冗余,增加可复用性。
  • 代码管理:模块化后的代码更加易于组织代码结构和管理。

模块化的演进

JavaScript 诞生之初只是作为一个脚本语言来使用,做一些简单的表单校验等。所以代码量很少,最开始都是直接写到 <script> 标签里,如下所示:

<script>
  var name = 'ming';
  var age = 18;
</script>

随着业务进一步复杂,开发者们开始把 JavaScript 写到独立的 js 文件中,把每个文件看成是一个模块,并通过 <script> 标签引入各个文件,与 html 文件解耦。

<script src="./module1.js"></script>
<script src="./module2.js"></script>
<script src="./module3.js"></script>

这种形式的模块化带来了几个问题:1. 每个模块的接口通常是暴露在全局作用域下的,也就是定义在 window 对象中,会造成全局变量的污染。2. 模块之间的依赖关系模糊,需要关注各个模块的加载顺序。通过以下的一些方式可以优化这些问题。

Namespace 模式

通过简单的对象封装,可以避免一些变量和方法直接暴露在全局命名空间下,减少了全局变量污染的问题。问题:数据是不被保护的,模块内部的状态(变量)和方法可以被轻易的覆盖。

var mathModule = {
    name: 'math_module',
    add(a, b) {
        return a + b
    }
}
mathModule.add(1, 2)
// 覆盖模块的变量和方法
mathModule.name = ''
mathModule.add = function() { }

函数作用域与IIFE

函数作用域下定义的变量只能在函数内部进行访问,要想访问这个变量的值,只能通过函数返回值进行获取。因此能将复杂的逻辑和局部变量封装在函数内部,并控制对外暴露的 API,就实现了简单的代码模块化。函数的返回值可以是对象和函数。

function getName() {
  var name = 'my_name'
  return name
}
console.log(name) // undefined
var result = getName()

// 返回对象
function getModule() {
  var name = 'my_name'
  return {
    getName() {
      return name;
    },
    editName(newName) {
      name = newName
      console.log('new name:' + name)
    }
  }
}
var module = getModule()
console.log(module.getName()) // my_name
module.editName('my_new_name') // new name: my_new_name

通过函数访问外部函数定义的变量,实际上是利用了闭包closure)的特性。类似的,将函数作为构造函数,也能实现类似的效果。

function Module() {
  this.publicParam = 'public'
  var privateparam = 'private'

  this.getPrivate = function() {
    return privateparam
  }
}
var moduleA = new Module()
console.log(moduleA.publicParam) // public
console.log(moduleA.getPrivate()) // private

除了直接引用函数名称进行执行,也可以通过 IIFE 获取到函数的结果。

var result = (function() { 
  var name = 'my_name'
  return name
})()

IIFEImmediately Invoked Function Expression)立即执行函数表达式,即一个一旦被声明就会被立即执行的匿名函数。在单文件模块中,可以将对外暴露的API挂载到 window 上,并且将其他依赖作为参数传入。

<!-- 引入的js必须有一定顺序 -->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
  myModule.foo()
</script>
// module.js
(function(window, $) {
  let data = 'www.baidu.com'
  //操作数据的函数
  function foo() {
    // 用于暴露的函数
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
    // 用于暴露的函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露行为
  window.myModule = { foo, bar } // ES6写法
})(window, jQuery)
  • 优点:保证模块的独立性,使模块之间的依赖关系变得明显。
  • 问题:模块的加载有先后顺序,不能循环依赖(模块B依赖模块A,模块A不能依赖模块B)

模块化规范

以上是模块化萌芽阶段的一些尝试,可以看到要解决的问题包括:

  1. 如何安全的封装复杂的逻辑,而不污染模块外的代码
  2. 如何唯一地标识一个模块
  3. 如何优雅地暴露出模块的API,而不增加全局变量
  4. 如何方便地使用依赖的模块,并维护模块间的依赖关系

在解决这些问题的过程中,产生了一系列的模块化规范,或者说 JavaScript 文件之间相互依赖引用的一种通用的语法约定。

CommonJS

文档:wiki.commonjs.org/wiki/Module…

2009年 NodeJS 横空出世,开创了 JS 发展史的一个崭新纪元,人们可以用 JS 来编写服务端的代码了。最早在这篇文章开始:What Server Side JavaScript NeedsCommonJS 社区讨论并制定了服务端 JavaScript的一系列标准,包括模块、文件、IO、控制台等。

CommonJS 中规定每个文件是一个模块,会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外不可见。

Modules/1.0

Modules/1.0 规范包含以下内容:

  • 模块的标识应遵循一定的书写规则。
  • 定义全局函数 require(dependency),通过传入模块标识来引入其他依赖模块,执行的结果即为别的模块暴漏出来的 API
  • 如果被 require 函数引入的模块中也包含外部依赖,则依次加载这些依赖。
  • 如果引入模块失败,那么 require 函数应该抛出一个异常。
  • 模块通过变量 exports 来向外暴露 APIexports 只能是一个 Object 对象,暴露的 API 须作为该对象的属性。

使用方法

  1. 导出模块

通过module.exports可以导出模块中的内容

module.exports = { 
  name: 'commonJS', 
  add: function(a, b) { 
    return a + b 
  } 
} 

模块内部会有一个 module 对象用于存放当前模块的信息,可以理解成在每个模块的最开始定义了以下对象:

var module = { } 
module.exports = { } 
// module.exports 和 exports指向同一片内存
exports = module.exports
  1. 导入模块

使用 require 进行模块导入

// 引入自定义的模块时,参数包含路径,可省略.js
var math = require('./math')
math.add(1, 2) // 3


// 或者
var add = require('./math').add
exports.increment = function(val) {
  return add(val, 1)
}

引入核心模块时,不需要带路径,如 var http = require('http')

require('./abc') 查找步骤:

  1. abc 当作文件在对应目录查找
    • 有后缀:按照后缀查找相应的文件
    • 无后缀:先查找 abc 文件,然后找 abc.js 文件,然后找 abc.json 文件,最后找 abc.node 文件
  2. 没找到对应文件,将 abc 作为目录
    • 查找 abc/index.js 文件,然后找 abc/index.json 文件,最后找 adc.node 文件
  3. 不是路径也不是核心模块,引入第三方库
    • const axios = require('axios')

require 一个模块时会有两种情况:

  • 第一次被加载: 首先执行该模块,然后导出内容
  • 曾被加载过: 这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果

多次加载,会缓存,最终只会运行一次。原理:每个模块都有一个 module 的属性,false 表示还没加载完,true 表示加载完毕

// a.js 
console.log('a模块被加载了') 
module.exports = { 
  name: 'commonJS', 
  add: function(a, b) { 
    return a + b; 
  } 
} 
 
// index.js 
const a = require('./a'); 
const sum = a.add(2, 3); 
console.log(sum); 
 
const moduleA = require('./a'); 
console.log(moduleA.name); 

/**
 * 输出:
 * 1. a模块被加载了 
 * 2. 5 
 * 3. commonJS 
 */

循环引入的模块加载顺序

图结构分为:深度优先和广度优先,node 采用深度优先。

main / a / c / d / e / b

// index.js
const main = require('./main')
// main.js
const a = require('./a')
const b = require('./b')
// a.js
const c = require('./c')
const d = require('./d')
// b.js
const c = require('./c')
const e = require('./e')
// c.js
const d = require('./d')
// d.js
const e = require('./e')

优点

  • 简单易用。
  • 解决了模块依赖的问题
  • 减少了全局变量污染

缺点

  • 无法在浏览器端使用。
  • 无法非阻塞的并行加载多个模块

浏览器端发展

Modules/1.0 规范源于服务端,无法直接用于浏览器端,原因:

  1. 外层没有 function 包裹,变量全暴露在全局。
  2. 资源的加载方式与服务端完全不同。

对于第2点,服务端 require 一个模块,直接就从硬盘或者内存中读取了,消耗的时间可以忽略。而浏览器则不同,需要从服务端来下载这个文件,然后运行里面的代码才能得到API,需要花费一个 http 请求的时间,也就是说,require 后面的一行代码,需要资源请求完成才能执行。由于浏览器端是以插入 <script> 标签的形式来加载资源的(ajax 方式不行,有跨域问题),没办法让代码同步执行,所以像 commonjs 那样的写法会直接报错。

所以,社区意识到要想在浏览器环境中也能模块化,需要对规范进行升级。而就在社区讨论制定下一版规范的时候,内部发生了比较大的分歧,出现了三个主要的流派:

  1. Modules/1.x

在现有基础上进行改进即可满足浏览器端的需要,既然浏览器端需要 function 包装,需要异步加载,那么新增一个方案,能把现有模块转化为适合浏览器端的就行了。基于这个主张,制定了 Modules/Transport 规范,即先通过工具把现有模块转化为适合浏览器环境使用的模块再使用的方式。代表工具:browserify

目前的最新版是 Modules/1.1.1,增加了一些 require 的属性,以及模块内增加 module 变量来描述模块信息,变动不大。

  1. Modules/Async

这个流派认为浏览器与服务器环境差别太大,不能沿用旧的模块标准。既然浏览器必须异步加载代码,那么模块在定义的时候就必须指明所依赖的模块,然后把本模块的代码写在回调函数里。模块的加载也是通过下载/回调这样的过程来进行,这个思想就是 AMD 的基础。

  1. Modules/2.0

介于前两者之间,既不想丢掉旧的规范,也不想像 AMD 那样推倒重来。需要吸收两者的优点:

  • Modules/1.0:比如通过 require 来声明依赖的方式
  • Modules/Async:模块的预先加载以及通过 return 可以暴露任意类型的数据,而不是像 1.0 那样 exports 只能为object

因此他们制定了 Modules/Wrappings 规范,包括:

  1. 全局有一个 module 变量,用来定义模块
  2. 通过 module.declare 方法来定义一个模块
  3. module.declare 接收一个参数,那就是模块的 factory
    • 对象:那么模块输出就是此对象
    • 函数:参数为 require, exports, module,用来引入其他依赖和导出本模块API。如果函数最后明确写有 return 数据,那么 return 的内容即为模块的内容。
// 可以使用exprots来对外暴露API
module.declare(function( require, exports, module ){
  exports.foo = 'bar'
})
// 也可以直接return来对外暴露数据
module.declare(function(require) {
  return { foo: 'bar' }
})

AMD

AMDAsync Module Definition) 代表作 RequireJS

规范

  • 模块的标识遵循 CommonJS Module Identifiers
  • 定义全局函数 define(id, dependencies, factory),用于定义模块。dependencies 为依赖的模块数组,在 factory 中需传入形参与之一一对应。
  • 如果 dependencies 的值中有 requireexportsmodule,则与 CommonJS 中的实现保持一致。
  • 如果 dependencies 省略不写,则默认为 ['require', 'exports', 'module']factory 中也会默认传入三者。
  • 如果 factory 为函数,模块可以通过以下三种方式对外暴漏 APIreturn 任意类型;exports.XModule = XModulemodule.exports = XModule
  • 如果 factory 为对象,则该对象即为模块的导出值。

模块定义

define({
    method1: function() {},
    method2: function() {},
});

// 函数的返回值就是输出的模块
define(function () {
    return {
        method1: function() {},
        method2: function() {},
    };
});

有依赖的模块

define(['module1', 'module2'], function(m1, m2) { });
// module1模块和module2模块指的是,当前目录下的module1.js文件和module2.js文件,等同于写成['./module1', './module2']

模块使用

require(['foo', 'bar'], function ( foo, bar ) {
    foo.doSomething();
});

优缺点

优点

  • 可以用于浏览器。
  • 异步加载模块。
  • 可以并行加载多个模块。

缺点

  • 提高了开发成本。
  • 不能按需加载,而是提前加载所有的依赖。

RequireJS2.0 开始,也改成了可以延迟执行。

CMD

CMDCommon Module Definition)是 sea.js 在推广过程中对模块定义的规范化产出,属于 CommonJS 的一种规范。

模块定义

define(function (require, exports, module) {
  var add = function (a, b) {
    return a + b;
  }
  exports.add = add;
})

模块使用

seajs.use(['math.js'], function (math) {
  var sum = math.add(1, 2);
})

CMD和AMD的区别

  1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible
  2. CMD 推崇依赖就近,AMD 推崇依赖前置。
//CMD默认推荐的是
define(function (require, exports, module) {
  var a = require('./a')
  a.doSomething()
  var b = require('./b') //依赖就近,按需加载,需要哪个写哪个
  b.doSomething()
})
//AMD默认推荐的是
define(['./a', './b'], function (a, b) { //依赖前置,必须一开始就写好
  a.doSomething()
  b.doSomething()
})

虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。

  1. AMDAPI 默认是一个当多个用,CMDAPI 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 requireCMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。

另外,SeaJSRequireJS 的差异,可以参考: github.com/seajs/seajs…

UMD

UMDUniversal Module Definition)是一种 JavaScript 通用模块定义规范,让你的模块能在 JavaScript 所有运行环境中发挥作用。规则如下:

  1. 优先判断是否存在 exports 方法,如果存在,则采用 CommonJS 方式加载模块。
  2. 其次判断是否存在 define 方法,如果存在,则采用 AMD 方式加载模块。
  3. 最后判断 global 对象上是否定义了所需依赖,存在则直接使用,反之则抛出异常。
(function(global, factory) {
  if(typeof module === 'object' && typeof module.exports === 'object') {
    // 判断CommonJS模块规范,node.js环境
    module.exports = factory(require('dep'))
  } else if(typeof define === 'function' && define.cmd) {
    // 判断CMD模块规范,如 sea.js
    define(function(require, exports, module) {
      module.exports = factory(require('dep'))
    })
  } else if(typeof define === 'function' && define.amd) {
    // 判断AMD模块规范,如 require.js
    define(['dep'], factory)
  } else {
    // 没有模块环境,直接挂在到全局对象上
    global.umdModule = factory(global.dep)
  }
})(this, function(dep) {
  // factory: 负责返回导出的内容(对象,函数,变量等)
  // dep 依赖
  return {
    name: '我是一个umd模块'
  }
})

ES Module

2015年6月,由TC39标准委员会正式发布了 ES6ECMAScript 6.0),从此 JavaScript 语言才具备了模块这一特性。ES Module 也是将每个文件作为一个模块,每个模块拥有自身的作用域,不同的是导入、导出语句。在 ES6 版本中 importexport 也作为了保留关键字(CommonJS 中的 module 并不属于关键字)。

Node.js 默认只支持 ESM 语法,解决方法:

  • package.json 中添加 "type": "module"
  • esm 模块的后缀改为 .mjs

优缺点

优点:

  • 语法层面的支持,使用简单
  • 死代码检测和排除
  • 模块变量类型检查
  • 编译器优化

缺点:

  • 浏览器还没有完全兼容,必须通过工具转换成标准的 ES5 后才能正常运行

导出模块

使用 export 命令来导出模块:

// 具名导出
export const name  = 'es6模块';
export const add = (a, b) =>  a + b
// 或
const name = 'es6模块'
const add = (a, b) =>  a + b
export { name, add }
// 可以用 as 关键字对变量重命名
export { add as sum }

// 默认导出
// 可以理解为对外输出了一个名为 `default` 的变量
export default {
  name: 'es6模块',
  add: function(a, b) {
    return a + b
  }
}

export 命令规定的是对外的接口,必须与模块内部的变量建立对应关系

// 直接输出 1,报错
export 1;

// 通过变量m,还是直接输出 1。1只是一个值,不是接口,报错
var m = 1;
export m;

导入模块

使用 import 语法导入模块,后面可以自由指定模块名

// 带有命名导出的模块
import { readFile } from 'fs'
// 默认导出的模块
import Vue from 'vue'

// 与命名导出类似,可以通过 as 关键字对导入的变量重命名
import { name, add as sum } from './a'

// 在导入多个变量时,可以采用整体导入的方式
import * as a from './a'
console.log(a.add(2,3))
console.log(a.name)

import 命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

import { a } from './xxx.js'
a = {} // Syntax Error : 'a' is read-only;

// 如果 a 是个对象,改写属性是允许的
import { a } from './xxx.js'
a.foo = 'hello' // 合法操作

默认导出和导入

在使用 export default Object 的时候,有时下意识会这么用:

// 导出 a.js
const a = 1
const b = 2
export default { a, b }
// 导入
import { a, b } from './a'
console.log(a, b)

按照 esm 标准,这里的 a/b 应该是 undefined,这个问题是对于对象解构(obejct destruct)和具名导出(named export)语法的误解,虽然两者长得一摸一样,但是使用的上下文不一样。正确的用法是:

// 导出 a.js
const a = 1
const b = 2
export default { a, b }
// 导入
import lib from './a'
const { a, b } = lib

// 或不使用default导出
export const a = 1
export const b = 1

import * as lib from './a'
const { a, b } = lib

最佳实践是:尽量不要使用 default export

反过来默认导入一个不含 default 导出的模块也会报错。

// lib.js
export const a = 1
export const b = 2

import lib from './lib'
// SyntaxError: The requested module './lib.js' does not provide an export named 'default'

复合写法

export { foo, bar } from 'my_module'

// 可以简单理解
import { foo, bar } from 'my_module'
export { foo, bar }

// 整体输出
export * from 'my_module'

// 接口重命名
export { foo as myFoo } from 'my_module'

// 具名接口改为默认接口
export { es6 as default } from './someModule'
// 等同于
import { es6 } from './someModule'
export default es6

// 默认接口改为具名接口
export { default as moduleB } from 'module_b'

import()函数

ES2020 提案引入 import() 函数,支持动态加载模块。import 命令能够接受什么参数,import() 函数就能接受什么参数,两者区别主要是后者为动态加载。import() 返回一个 Promise 对象。适用场景:

  1. 按需加载
button.addEventListener('click', event => {
  import('./dialogBox.js').then(dialogBox => {
    dialogBox.open();
  }).catch(error => {
    /* Error handling */
  })
})
  1. 条件加载
if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}
  1. 动态的模块路径

import() 允许模块路径动态生成:

import(getPath()).then(...);

cjsesm的互操作性问题

esm引用cjs模块

esm 模块能顺利的引用 cjs 模块,但是对于具名导出,只能 import default

// lib.cjs
module.exports = {
  a: 100
}
// index.js
import { a } from './lib.cjs'
/**
 * SyntaxError: Named export 'a' not found. 
 * The requested module './lib.cjs' is a CommonJS module, which may not support all module.exports as named exports
 * CommonJS modules can always be imported via the default export, for example using:
 * import pkg from './lib.cjs';
 * const { a } = pkg;
 */

这是因为 esm 需要在编译阶段进行静态分析,检查模块上调用了 import/export 的地方,顺藤摸瓜地把依赖模块一个个异步、并行的下载下来,在此阶段 esm 加载器不会执行任何依赖模块代码,只会进行语法检错、确定模块的依赖关系、确定模块输入和输出的变量,而 cjs 的脚本在运行阶段才能计算出它们的 named exports,会导致 esm 在编译阶段无法进行分析。

如果是在 Typescript 中使用 cjs 模块,然后 tsc 编译,有以下场景和方案:

// 1. namespace import
// lib.js
exports.a = 100
// index.ts
import lib from './lib'
/**
 * error TS1192:
 * Module './lib' has no default export.
 */

// 需要使用 namespace import
import * as lib from './lib'
// tsc 得到
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const lib = require("./lib");
console.log(lib); // { a: 100 }
// 2. default exports + esModuleInterop
// lib.js
module.exports = {
  a: 100
}
// index.ts
import lib from './lib'
/**
 * error TS1259: 
 * Module './lib' can only be default-imported using the 'esModuleInterop' flag
 */

/**
 * 设置 tsconfig.compilerOptions.esModuleInterop = true 编译成功
 * 
 * Emit additional JavaScript to ease support for importing CommonJS modules. 
 * This enables allowSyntheticDefaultImports for type compatibility.
 * See more: https://www.typescriptlang.org/tsconfig#esModuleInterop
 */
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const lib_1 = __importDefault(require("./lib"));
console.log(lib_1.default); // { a: 100 }
// 3. 在库代码生成的时候,做以下处理,例如axios中:
var axios = { /** XXXX */ };
module.exports = axios;
// Allow use of default import syntax in TypeScript
module.exports.default = axios;
// 或 module.exports.default = module.exports 

/**
 * 上面的例子使用 
 * var mode = { a: 100 }
 * module.exports = mode
 * module.exports.default = mode
 */
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const lib_1 = require("./lib");
console.log(lib_1.default);

/**
 * 打印出来的内容是:{ a: 100, default: [Circular *1] }
 * 1. 丑陋的导出对象,对象内存在循环引用
 * 2. 对这个对象JSON序列化会破坏vscode的自动补全 https://github.com/sindresorhus/mem/issues/31
 */
cjs引用esm模块

使用 require 方法不能引入 esm 模块

// lib.mjs
export const a = 100
// index.js
const lib = require('./lib.mjs')
/**
 * node:internal/modules/cjs/loader:979 
 * throw new ERR_REQUIRE_ESM(filename, true);
 * 
 * Error [ERR_REQUIRE_ESM]: require() of ES Module ./lib.mjs not supported.
 * Instead change the require of ./lib.mjs to a dynamic import() which is available in all CommonJS modules.
 */

可以使用 cjs 内置的动态 import 方法:

import('./lib.mjs').then(lib => {
  console.log(lib) // [Module: null prototype] { a: 100 }
  console.log(lib.a) // 100
})

开源项目当然不能强制要求用户该用这种方式,又得借助 rollup 等工具将其编译为 cjs

'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const a = 100;
exports.a = a;

如果 esm 使用的是默认导出,编译为 cjs 将得到:

// lib.js
export default {
  a: 100
}

// tsc 编译
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = {
  a: 100
};

// cjs 中使用
const lib = require('./lib')
console.log(lib) // { default: { a: 100 } }
/**
 * 需要使用 lib.default.a 访问
 * 而不是预想的 lib.a
 */

如果使用 rollup 打包使用 auto 模式可以规避这个问题,有如下警告:

(!) Entry module "src/index.js" is implicitly using "default" export mode, which means for CommonJS output that its default export is assigned to "module.exports". For many tools, such CommonJS output will not be interchangeable with the original ES module. If this is intended, explicitly set "output.exports" to either "auto" or "default", otherwise you might want to consider changing the signature of "src/index.js" to use named exports only.
rollupjs.org/guide/en/#o…

// 省略或设置 `output.exports` 为 `auto | default`
'use strict';
var lib = {
  a: 100
};
module.exports = lib;

// 设置为 output.exports = 'named' 则和上面tsc的相同

CommonJS区别

  1. 加载逻辑不同
  • ES Module: 编译时进行静态分析,而不会执行,确定要加载的模块进行语法检错、确定依赖关系、确定输入和输出的变量。
  • CommonJS: require() 是一个同步接口,它会直接从磁盘(或网络)读取依赖模块并立即执行对应的脚本。
  1. 不同的模式
  • ES Module: 默认使用了严格模式(use strict), this 不再指向全局对象(而是undefined)...
  • CommonJS: 默认不使用严格模式,所以编译结果通常带 'use strict';
  1. ES Module 支持顶级 awaitCommonJS 不支持
  2. ES Module 缺乏 __filename__dirname

参考