前端工程化实战 - 模块化开发

一、模块化概述

  • 模块化可以说是最重要的前端开发范式之一。随着前端应用的日益复杂,我们的项目代码已经逐渐膨胀到了不得不花大量时间管理的程度了
  • 而模块化就是一种最主流的代码组织方式,它通过把复杂代码按照功能的不同,划分为不同的模块单独维护的这种方式提高我们的开发效率,降低维护成本
  • 但就模块化这个词而言,它仅仅是一种思想或者说是一种理论,并不包含具体实现

二、模块化演变过程

早期的前端技术标准根本没有预料到前端行业会有今天这样一个规模,所以很多设计上的遗留问题就导致我们现在去实现前端模块化的时候会遇到很多的问题。虽然现如今这些问题都被一些标准或者工具去解决了,但是它的解决的一个演进过程是值得我们去思考的

  • 最早期,JavaScript 当中的模块化实际上就是基于文件划分的方式实现的,这也就是 Web 中最原始的模块系统
  • 具体做法就是将每个功能以及它相关的状态数据单独存放到不同的文件当中,去约定每一个文件就是一个独立的模块。我们去使用这个模块就是将这个模块引入到页面当中,一个 script 标签就对应一个模块。再在代码中直接调用模块中的全局成员,这个成员有可能是一个变量,也有可能是一个函数
  • 这种方式的缺点也十分明显,就是所有的模块都直接在全局范围去工作,并没有一个独立的私有空间,这样就导致模块当中所有的成员都可以在模块外部被任意的访问或修改,也就是污染全局作用域,而且模块多了过后还会存在命名冲突问题,我们也无法管理模块依赖的关系。它完全依靠约定,项目一但上了体量,就彻底不行了

第一阶段

// module a 相关状态数据和功能函数

var name = 'module-a'

function method1 () {
  console.log(name + '#method1')
}

function method2 () {
  console.log(name + '#method2')
}
// module b 相关状态数据和功能函数

var name = 'module-b'

function method1 () {
  console.log(name + '#method1')
}

function method2 () {
  console.log(name + '#method2')
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Modular evolution stage 1</title>
</head>
<body>
  <h1>模块化演变(第一阶段)</h1>
  <h2>基于文件的划分模块的方式</h2>
  <p>
    具体做法就是将每个功能及其相关状态数据各自单独放到不同的文件中,
    约定每个文件就是一个独立的模块,
    使用某个模块就是将这个模块引入到页面中,然后直接调用模块中的成员(变量 / 函数)
  </p>
  <p>
    缺点十分明显:
    所有模块都直接在全局工作,没有私有空间,所有成员都可以在模块外部被访问或者修改,
    而且模块一段多了过后,容易产生命名冲突,
    另外无法管理模块与模块之间的依赖关系
  </p>
  <script src="module-a.js"></script>
  <script src="module-b.js"></script>
  <script>
    // 命名冲突
    method1()
    // 模块成员可以被修改
    name = 'foo'
  </script>
</body>
</html>
  • 所以就有了第二阶段,在第二阶段当中,我们约定每一个模块只暴露一个全局对象,所有模块的成员都挂载到对象下面
  • 具体做法就是在第一阶段的基础上,通过将每个模块包裹为一个全局对象的形式实现,有点类似于为模块内的成员添加了命名空间的感觉
  • 通过命名空间减小了命名冲突的可能,但是同样没有私有空间,所有模块成员也可以在模块外部被访问或者修改,而且也无法管理模块之间的依赖关系

第二阶段

// module a 相关状态数据和功能函数

var moduleA = {
  name: 'module-a',

  method1: function () {
    console.log(this.name + '#method1')
  },

  method2: function () {
    console.log(this.name + '#method2')
  }
}
// module b 相关状态数据和功能函数

var moduleB = {
  name: 'module-b',

  method1: function () {
    console.log(this.name + '#method1')
  },

  method2: function () {
    console.log(this.name + '#method2')
  }
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Modular evolution stage 2</title>
</head>
<body>
  <h1>模块化演变(第二阶段)</h1>
  <h2>每个模块只暴露一个全局对象,所有模块成员都挂载到这个对象中</h2>
  <p>
    具体做法就是在第一阶段的基础上,通过将每个模块「包裹」为一个全局对象的形式实现,
    有点类似于为模块内的成员添加了「命名空间」的感觉。
  </p>
  <p>
    通过「命名空间」减小了命名冲突的可能,
    但是同样没有私有空间,所有模块成员也可以在模块外部被访问或者修改,
    而且也无法管理模块之间的依赖关系。
  </p>
  <script src="module-a.js"></script>
  <script src="module-b.js"></script>
  <script>
    moduleA.method1()
    moduleB.method1()
    // 模块成员可以被修改
    moduleA.name = 'foo'
  </script>
</body>
</html>
  • 到了第三阶段,第三阶段使用立即执行函数的方式为模块提供私有空间
  • 具体做法就是将模块中每一个成员都放在一个函数提供的私有作用域中,对于需要暴露给外部的成员可以通过挂载到全局对象的方式来实现,这种方式实现了私有成员的概念,也就是说私有成员只能是模块内部的成员通过闭包来访问,而在外部是无法访问的
  • 这样就确保了变量的安全,而且还可以利用自执行函数的参数去作为依赖声明使用,就使得每一个模块之间的依赖关系变得更加明显

第三阶段

// module a 相关状态数据和功能函数

;(function () {
  var name = 'module-a'
  
  function method1 () {
    console.log(name + '#method1')
  }
  
  function method2 () {
    console.log(name + '#method2')
  }

  window.moduleA = {
    method1: method1,
    method2: method2
  }
})()
// module b 相关状态数据和功能函数

;(function () {
  var name = 'module-b'
  
  function method1 () {
    console.log(name + '#method1')
  }
  
  function method2 () {
    console.log(name + '#method2')
  }

  window.moduleB = {
    method1: method1,
    method2: method2
  }
})()
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Modular evolution stage 3</title>
</head>
<body>
  <h1>模块化演变(第三阶段)</h1>
  <h2>使用立即执行函数表达式(IIFE:Immediately-Invoked Function Expression)
  为模块提供私有空间</h2>
  <p>
    具体做法就是将每个模块成员都放在一个函数提供的私有作用域中,
    对于需要暴露给外部的成员,通过挂在到全局对象上的方式实现
  </p>
  <p>
    有了私有成员的概念,私有成员只能在模块成员内通过闭包的形式访问。
  </p>
  <script src="module-a.js"></script>
  <script src="module-b.js"></script>
  <script>
    moduleA.method1()
    moduleB.method1()
    // 模块私有成员无法访问
    console.log(moduleA.name) // => undefined
  </script>
</body>
</html>
  • 以上这几个阶段就是早期开发者在没有工具和规范的情况下对模块化的落地方式
  • 这些方式确实解决了前端领域去实现模块化的问题,但是它仍然存在一些没有解决的问题

三、模块化规范的出现

在模块化当中针对模块加载的问题,在上述几种方式中都是通过 script 标签手动引入的,这也就意味着模块加载并不受代码的控制,一旦时间久了过后,维护就会非常麻烦。所以我们需要一些基础的公共代码去实现自动靠代码来加载模块(模块化标准 + 模块加载器)

CommonJS 规范

它是 Node.js 当中所提出的一套标准,我们在 Node.js 当中所有的模块代码必须要遵循 CommonJS 规范

这个规范约定了:

  • 一个文件就是一个模块
  • 每个模块都有单独的作用域
  • 通过 module.exports 导出成员
  • 通过 require 函数载入模块

如果我们想要在浏览器端也使用这个规范的话,就会出现一些问题,我们知道 CommonJS 是以同步模式加载模块,因为 node 的执行机制是在启动时加载模块,执行过程当中是不需要加载的,它只会使用到模块。如果换到浏览器端使用 CommonJS 规范的话,必然导致效率低下,因为每一次页面加载都会导致大量的同步模式请求出现

所以在早期的前端模块化当中并没有选择 CommonJS 这个规范,而是专门为浏览器端重新设计了一个规范,叫做 AMD(Asynchronous Module Definition - 异步的模块规范),而且同期还推出了一个非常出名的库 Require.js,它实现了 AMD 规范,另外它本身又是一个非常强大的模块加载器

Require.js 规范

  • 在 AMD 这个规范当中,约定每一个模块都必须通过 define 这个函数定义,默认可以接收两个参数,也可以传递三个参数
  • 第一个参数是模块的名字
  • 第二个参数是一个数组,用于声明模块依赖项
  • 第三个参数是一个函数,函数的参数与前面的依赖项一一对应,每一项分别为依赖项这个模块导出的成员,这个函数的作用可以以理解为,为当前的这个模块提供一个私有的空间
  • 如果需要在这个模块当中向外部导出一些成员,可以通过 return 的方式实现
// 定义一个模块
define('module1', ['jquery', './module2'], function ($, module2) {
  return {
    start: function () {
      $('body').animate({ margin: '200px' })
      module2()
    }
  }
})
  • 除此之外,Require.js 当中还提供一个 require 函数用来自动加载模块,用法与 define 函数类似。区别在于 require 函数只是用来加载模块,而 define 函数是用来定义模块的
  • 当 Require.js 需要加载一个模块的话,内部会自动的创建一个 script 标签去发送对应脚本文件的请求,并且执行相应模块代码
// 载入一个模块
require(['./module1'], function (module1) {
  module1.start()
})
  • 目前绝大多数第三方库都支持 AMD 规范,但是 AMD 使用起来相对复杂,如果项目中的模块划分的非常细致的话,模块 JS 文件请求频繁
  • 同期出现的还有淘宝推出的 Sea.js + CMD ,可以算是一个重复的「轮子」。它当时的想法就是希望让 CMD 写出来的代码尽可能与 CommonJS 类似,从而减轻开发者的学习成本。这种方式后来也被 Require.js 兼容

四、模块化标准规范

随着技术的发展,JavaScript 的标准也在逐渐完善。而模块化的实现方式相对以往已经有了很大的变化。目前针对前端模块化的最佳实现方式也逐渐统一

  • 在 Node.js 中会遵循 CommonJS 规范,而在浏览器环境中可以采用 ES Modules 的规范
  • 在Node.js 中使用 CommonJS 没有任何环境问题,它是内置模块系统,我们直接去遵顼CommonJS 规范去使用 require 载入模块,通过 module.export 导出模块就可以了

在这里插入图片描述

  • 但是对于 ES Modules,其情况就相对复杂一些,我们知道,ES Modules 是 ECMAScript 2015(ES6) 当中定义的最新的模块系统,也就是说它是最近几年才被定义的一个标准,所以它肯定会存在各种各样的环境兼容性问题
  • 早期在这个标准刚推出的时候,所有主流浏览器基本都是不支持这样一个特性的,但是随着 Webpack 等一系列打包工具的流行,这一规范才逐渐开始普及

在这里插入图片描述

  • 截止到目前,ES Modules 可以说是最主流的前端模块化方案了。相比于 AMD 这种社区提出来的开发规范,ES Modules 在语言层面实现了模块化,更为完善
  • 现如今绝大多数浏览器已经开始支持 ES Modules 这个特性了,原生支持就意味着我们可以在以后直接使用这样一个特性开发我们的网页应用

五、ES Modules

5.1 ES Modules 基本特性

通过给 script 添加 type = module 的属性,就可以以 ES Module 的标准执行其中的 JS 代码了

<!-- 通过给 script 添加 type = module 的属性,
就可以以 ES Module 的标准执行其中的 JS 代码了 -->
  <script type="module">
    console.log('this is es module')
  </script>
  • ESM 自动采用严格模式,忽略 ‘use strict’
<!-- ESM 自动采用严格模式,忽略 'use strict' -->
<script type="module">
// 严格模式下不能在全局使用 this
   console.log(this); // undefined
</script>
  • 每个 ES Module 都是运行在单独的私有作用域中
<!-- 每个 ES Module 都是运行在单独的私有作用域中 -->
<script type="module">
   var foo = 100;
   console.log(foo); // 100
</script>
<script type="module">
   console.log(foo); // foo is not defined
</script>
  • ESM 是通过 CORS 的方式请求外部 JS 模块的(首先要保证服务端支持 CORS)
<!-- ESM 是通过 CORS 的方式请求外部 JS 模块的 -->
    <script
      type="module"
      src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"
    ></script>
  • ESM 的 script 标签会延迟执行脚本(等同于 script 标签的 defer 属性)
 <script type="module" src="demo.js"></script>
    <p>需要显示的内容</p>

等同于

 <!-- ESM 的 script 标签会延迟执行脚本 -->
<script defer src="demo.js"></script>
<p>需要显示的内容</p>

5.2 ES Modules 导出

  • 如果我们需要对外提供某些成员,必须使用 export 关键词修饰变量、函数、类等的声明,此时就可以通过 import 载入其他模块当中通过 export 导出的成员了

导出

export var name = 'foo module'

export function hello () {
  console.log('hello')
}

export class Person {}

导入

import { name, hello, Person } from './module.js'
console.log(name, hello, Person)
  • 除此之外,我们还可以通过 export { } 的形式导出
var name = 'foo module'

function hello () {
  console.log('hello')
}

class Person {}

export { name, hello, Person }
  • 通过上述这种方式使用 as 关键词为输出的成员进行重命名
export {
  name as fooName,
  hello as fooHello
}

重命名使用过程中有一个特殊情况,一旦将导出成员的名称设置为 default 的话,这个成员就会作为当前模块默认导出的成员,导入这个成员的时候就必须给这个成员重命名,因为 default 是一个关键词,不能将其作为变量使用

导出

export {
  name as default,
  hello as fooHello
}

导入

import { default as fooName } from './module.js'
console.log(fooName)
  • 在 ES Modules 当中还为 default 成员单独设计了一个特殊的用法(默认导出)

导出

export default name

导入

// fooName 这个变量名可以自己定义
import fooName from "./module.js";

5.3 ES Modules 导入导出注意事项

  • 在使用 export 导出模块当中的成员时,使用了 export { },这种方式很对人会误认为 export 后面跟着的 { } 是一个对象的字面量,从而误认为导入的是对这个对象的解构,export { } 是一个固定的语法
  • 如果你需要导出一个对象,则可以通过 export default { }
var name = "foo module";
export default { name }
// 如果使用 import { fooName } from "./module.js" 会直接报错

/*  报错:The requested module './module.js' 
does not provide an export named 'fooName' */

import fooName from "./module.js";
console.log(fooName); // {name: 'foo module'}
  • 这就意味着 import { } 也不是解构,而是一个固定的语法,用来提取导出成员
  • 在 ES Modules 当中导出成员时,导出的是这个成员的引用,其这个引用关系是只读的,并不能在模块外部去修改
var name = "jack";
var age = 18;

// 这里的 `{ name, hello }` 不是一个对象字面量,
// 它只是语法上的规则而已
export { name, age };

// export name // 错误的用法

// export 'foo' // 同样错误的用法

setTimeout(function () {
  name = "ben";
}, 1000);
// CommonJS 中是先将模块整体导入为一个对象,然后从对象中结构出需要的成员
// const { name, age } = require('./module.js')

// ES Module 中 { } 是固定语法,就是直接提取模块导出成员
import { name, age } from './module.js'

console.log(name, age) // jack 18  --- 打印结果

// 尝试修改name
// name = 'tom' // 直接报错

// 导入成员并不是复制一个副本,
// 而是直接导入模块成员的引用地址,
// 也就是说 import 得到的变量与 export 导入的变量在内存中是同一块空间。
// 一旦模块中成员修改了,这里也会同时修改,
setTimeout(function () {
  console.log(name, age) // ben 18   --- 打印结果
}, 1500)

5.4 ES Modules 导入

  • 在 import 在导入模块时,from 写的是导入模块的路径,它是一个字符串,在这个字符串当中,必须使用完整的文件名称,不能省略扩展名
import { name } from './module.js'
  • 在 ES Modules 原生工作中不能自动载入 index.js
  • 如果我们使用的是相对路径时的 ’./…’ 无法省略,如果省略以字母开头,ES Moudles 会认为是在加载第三方模块
import { lowercase } from './utils/index.js'
  • 除了使用相对路径,我们还可以使用绝对路径,甚至是完整的 URL 去加载模块,这也意味我们可以直接引用 CDN 上的模块文件
import { name } from '/04-import/module.js'
import { name } from 'http://localhost:3000/04-import/module.js'
  • 如果我们只需要去执行某个模块,并不需要提取其中的成员的话,可以保持 import 后的 { } 为空,且可以简写为 import '路径'
import {} from './module.js'
import './module.js' // 简写
  • 如果一个模块需要导出的的成员特别多,且在导入时都会用到,则可以使用 import * as 对象名(自定义) from '路径' 的方式将它们全部提取出来,每个导出的成员都会作为对象的属性出现
import * as mod from './module.js'
  • 如果模块路径是在运行阶段才知道的,或者当某些条件满足才去导入模块,此时不能使用 import 关键词,因为 import 关键词只能出现在最顶层(最外层作用域)此时就需要动态导入模块的机制了,ES Modules 提供了全局的 import 函数专门用来动态导入模块
  • 这个 import 函数返回的是一个 Promise,当次模块加载完成过后会自动执行 then() 当中指定的回调函数,模块对象可以通过参数获取
// 错误用法1
// var modulePath = './module.js'
// import { name } from modulePath
// console.log(name)

// 错误用法2
// if (true) {
//   import { name } from './module.js'
// }

import('./module.js').then(function (module) {
// 模块对象可以通过参数获取
   console.log(module)
})
  • 如果在一个模块中同时导出命名成员和默认成员
import { name, age, default as title } from './module.js'
import abc, { name, age } from './module.js' // 简写(abc名字可以自己定义)

5.5 ES Modules 导出导入成员

  • 除了导入模块,import 还可以配合 export 使用,效果就是将导入的结果直接作为当前模块的导出成员
  • 具体用法就是将 import 声明当中的 import 关键词修改为 export,这样一来,所有导入的成员将直接作为当前模块的导出成员,在当前作用域中就不再允许访问成员了
export { foo, bar } from './module.js'
  • 这样一个特性一般用于 index.js 文件:通过 index 文件把某一个目录下散落的一些模块通过这种方式组织到一起导出,方便外部的使用
  • 如果组件文件当中导出的是一个默认成员的话,就不能再通过提取的方式导出,就必须提取它的 default,并为 default 重命名,否则它会作为当前 index 文件的默认导出,外界就只能通过 defaule 使用了

在这里插入图片描述

index.js

export { default as Button } from './button.js'
export { Avatar } from './avatar.js'

app.js

import { Button, Avatar } from './components/index.js'

5.6 ES Modules 浏览器环境 Polyfill

因为 ES Modules 是 2014 年才被提出的,这也就意味着早期的浏览器不可能支持这一特性。IE 以及一些国产浏览器到目前为止也还未支持,所以我们在使用 ES Modules 时还是需要去考虑兼容性的

  • 我们可以通过使用一个 Polyfill 让我们在浏览器当中直接支持 ES Modules 中绝大多数特性。这个模块的名字叫做 ES Module Loader,这个模块实际上就是一个 JS 文件,具体用法就是将这个JS文件引入到页面当中,就可以让这个网页运行 ES Modules 了
  • ES Module Loader 的工作原理就是将浏览器当中不识别的 ES Modules 交给 Babel 转换,对于需要 import 进来的文件再通过 Ajax 的方式请求,把请求回来的代码再通过 Babel 转换从而支持 ES Modules
  • 如果在支持 ES Modules 的浏览器中代码会被执行两次。我们可以及借助 script 标签的一个新属性 nomodule 解决

具体用法

  • 通过 unpkg.com 提供的 cdn 服务拿到它下面所有的 JS 文件,通过 script 标签分别引入两个文件的路径
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>ES Module 浏览器环境 Polyfill</title>
</head>
<body>
  <script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
  <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
  <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
  <script type="module">
    import { foo } from './module.js'
    console.log(foo)
  </script>
</body>
</html>

这种方式只适合于开发阶段本地测试,在生产阶段千万不要使用它,因为它的原理都是在运行阶段动态地解析脚本,效率就会非常差。在生产阶段还是应该预先将这些代码编译出来,让它可以直接在浏览器当中工作

5.7 ES Modules in Node.js - 支持情况

Node 8.5 版本过后,内部已经以实验特性的方式支持 ES Modules 了。原来的 CommonJS 规范与现在的 ES Modules 它们之间的差距比较大,所以说目前这个特性,还处于一个过渡状态

  • 需要将扩展名更改为 .mjs(如果提示是否需要更新 import 路径,选择 no)

在这里插入图片描述

  • 启动 Node 时需要增加 --experimental-modules 的参数,代表启用 ES Module 实验特性
node --experimental-modules index.mjs
  • 可以通过 ES Modules 的方式载入原生模块以及第三方模块(这里提前安装了 lodash)
  • 在 ES Modules 中提取模块成员的用法是不能实现的,因为 import { } 内部并不是解构,而第三方模块导出的是一个对象,会作为默认成员导出,因此需要通过默认导入的方式导入成员
  • 但是我们可以通过提取的方式直接提取系统内置模块当中的成员,因为系统内置的模块官方做了兼容,对其内部成员单独进行了导出且作为默认对象导出

index.mjs

// 第一,将文件的扩展名由 .js 改为 .mjs;
// 第二,启动时需要额外添加 `--experimental-modules` 参数;

import { foo, bar } from './module.mjs'

console.log(foo, bar)

// 此时我们也可以通过 ESM 加载内置模块了
import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')

// 也可以直接提取模块内的成员,内置模块兼容了 ESM 的提取成员方式
import { writeFileSync } from 'fs'
writeFileSync('./bar.txt', 'es module working')

// 对于第三方的 NPM 模块也可以通过 ESM 加载
import _ from 'lodash'
_.camelCase('ES Module')

// 不支持,因为第三方模块都是导出默认成员
// import { camelCase } from 'lodash'
// console.log(camelCase('ES Module'))

5.8 ES Modules in Node.js - 与 CommonJS 交互

  • ES Module 中可以导入 CommonJS 模块

在这里插入图片描述
commonjs.js

// CommonJS 模块始终只会导出一个默认成员
module.exports = {
  foo: "commonjs exports value",
};
// 或
// exports.foo = "commonjs exports value"

es-module.mjs

// ES Module 中可以导入 CommonJS 模块
import mod from "./commonjs.js";
console.log(mod); // { foo: 'commonjs exports value' }

// 不能直接提取成员,注意 import 不是解构导出对象
// import { foo } from './commonjs.js'
// console.log(foo) // 报错
  • 不能在 CommonJS 模块中通过 require 载入 ES Module

commonjs.js

// 会报错
const mod = require("./es-module.mjs");
console.log(mod);

es-module.mjs

export const foo = 'es module export value'

总结

  • ES Modules 中可以导入 CommonJS 模块
  • CommonJS 中不能导入 ES Modules 模块
  • CommonJS 始终只会导出一个默认成员
  • 注意 import 不是解构导出对象

5.9 ES Modules in Node.js - 与 CommonJS 的差异

CommonJS 的内置成员

// 加载模块函数
console.log(require)

// 模块对象
console.log(module)

// 导出对象别名
console.log(exports)

// 当前文件的绝对路径
console.log(__filename)

// 当前文件所在目录
console.log(__dirname)

在ES Modules 中没有 CommonJS 中的模块全局成员

// ESM 中没有模块全局成员了

// 加载模块函数
// console.log(require)

// 模块对象
// console.log(module)

// 导出对象别名
// console.log(exports)

// 当前文件的绝对路径
// console.log(__filename)

// 当前文件所在目录
// console.log(__dirname)

// --------------------------------

// require, module, exports 自然是通过 import 和 export 代替

// __filename 和 __dirname 通过 import 对象的 meta 属性获取
// const currentUrl = import.meta.url
// console.log(currentUrl)

// 通过 url 模块的 fileURLToPath 方法转换为路径
import { fileURLToPath } from "url"; 
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url); // 将文件 url 转换成路径
const __dirname = dirname(__filename); // 将完整路径提取出文件夹部分
console.log(__filename);
console.log(__dirname);

5.10 ES Modules in Node.js - 新版本进一步支持

  • 在 Node 的最新版本当中进一步地支持了 ES Modules
  • Node v12 之后的版本,可以通过 package.json 中添加 type 字段为 module,将默认模块系统修改为 ES Module
  • 可以在终端使用 nvm use 版本号 切换 node 版本

在这里插入图片描述

package.json

{
  "type": "module"
}
  • 此时就不需要修改文件扩展名为 .mjs 了

module.js

export const foo = 'hello'

export const bar = 'world'

index.js

// Node v12 之后的版本,可以通过 package.json 中添加 type 字段为 module,
// 将默认模块系统修改为 ES Module
// 此时就不需要修改文件扩展名为 .mjs 了

import { foo, bar } from './module.js'

console.log(foo, bar)
  • 如果需要在 type=module 的情况下继续使用 CommonJS,需要将文件扩展名修改为 .cjs

common.cjs

// 如果需要在 type=module 的情况下继续使用 CommonJS,
// 需要将文件扩展名修改为 .cjs

const path = require('path')

console.log(path.join(__dirname, 'foo'))

5.11 ES Modules in Node.js - Babel 兼容方案

如果你使用的是早期的 Node.js 版本,可以使用 Babel 实现 ES Module 的兼容

  • Babel 是目前最主流的一款 JavaScript 编译器,它可以用来帮我们将一些使用了新特性的代码编译成当前环境支持的代码(以下测试用例使用的 node 版本是 8.0.0)

在这里插入图片描述

安装 babel

yarn add @babel/node @babel/core @babel/preset-env --dev

module.js

export const foo = 'hello'

export const bar = 'world'

index.js

// 对于早期的 Node.js 版本,可以使用 Babel 实现 ES Module 的兼容
import { foo, bar } from './module.js'

console.log(foo, bar)

运行

yarn babel-node index.js
  • 这里直接运行会报错,说 import 不被支持,这是因为 babel 是基于插件机制实现的,它的核心模块并不会去转换代码,具体转换代码中的每一个特性,是通过插件实现的

在这里插入图片描述

  • 之前安装的 preset-env 是一个插件集合,在这个插件集合包涵了最新的 JS 标准中的所有新特性,在这里我们可以借助于 preset 直接去把当前使用的 ES Modules 转换过来

在这里插入图片描述

运行命令修改

yarn babel-node index.js --presets=@babel/preset-env
  • 如果觉着每次手动传参麻烦,可以放到配置文件

.babelrc

// .babelrc
{
  "presets": [
    "@babel/preset-env"
  ]
}

运行命令

yarn babel-node index.js
  • 实际帮我们转换 ES Modules 特性的是一个插件,并不是preset,preset 只是一个集合,我们可以使用单独插件来转换(这里我们先移除项目中的 @babel/preset-env)
yarn remove @babel/preset-env
yarn add @babel/plugin-transform-modules-commonjs --dev

.babelrc

// .babelrc
// 替换掉之前写的 presets
{
  "plugins": [
    "@babel/plugin-transform-modules-commonjs"
  ]
}

运行命令

yarn babel-node index.js
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天内卷一点点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值