模块化开发

279 阅读24分钟

一、模块化概述

模块化开发-当下最重要的开发范式 随着前端应用的日益复杂,我们的代码已经膨胀到了不得不花大量的时间去管理的程度了。而模块化就是一种最主流的代码组织方式,它通过把复杂代码按照功能的不同划分不同的模块单独维护的方式提供开发的效率、降低维护成本。但就模块化这个名次来言,它仅仅是一个思想或者说一个理论并不包含具体的实现。 前端模块化就是在前端项目中实践模块化思想 我们下面从这几个方面来了解模块化思想及前端模块化的实践:

  • 模块化的演变过程:通过历史了解模块化存在的价值以及它解决的问题;
  • 模块化的标准:目前前端市面上模块化的一些标准和规范;
  • 常用的模块化打包工具;
  • 基于模块化化工具构建现代 Web 应用;
  • 打包工具的优化技巧;

二、模块化演变过程

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

  • Stage 1 -- 文件划分方式:这是 WEB 当中最原始的模块系统,具体的做法就是将每一个功能以及它相关的一些状态数据单独存放到不同的文件当中并约定每一个文件代表一个模块。使用时就就将这个模块文件引入到页面文件当中,一个 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>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.js

var name = 'module-a'

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

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

// module-b.js

var name = 'module-b'

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

function method2 () {
  console.log(name + '#method2')
}
  • Stage 2 -- 命名空间:第二个阶段在第一个阶段的基础上约定每一个模块只暴露一个全局的对象,所有的模块成员都挂载到这个对象下面。具体的做法就是在第一个阶段的基础上,通过将每一个模块包裹成为一个全局对象的方式去实现。这样有点类似于在模块内去为每个模块一些成员添加了命名空间,通过命名空间的方式我们就可以减小命名冲突的可能。但是这种方式仍然没有私用空间,所以说模块成员仍然可以在外部被访问、修改,另外模块之间的依赖关系没有得到解决
<!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.js

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

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

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

// module-b.js

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

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

  method2: function () {
    console.log(this.name + '#method2')
  }
}
  • Stage 3 -- IIFE:在第三阶段当中使用立即执行函数这种方式,去为模块提供私有空间。具体的做法就是将模块中每一个成员都放在一个函数提供的私有作用域当中,对于需要暴露给外部的成员可以通过挂载到全局对象上的方式去实现。这种方式实现了私有成员的概念,就是说模块的私有成员只能在模块内部通过闭包的方式去访问而在外部,是没有办法去使用使用的这样就确保了私有成员的安全。
<!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>
// module-a.js

;(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.js

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

  window.moduleB = {
    method1: method1,
    method2: method2
  }
})()

  • Stage 4 -- IIFE 依赖参数:可以利用自执行函数的参数去作为依赖声明去使用,这样的话就使得每一个模块之间的依赖关系就变得明显。
<!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 4</title>
</head>
<body>
  <h1>模块化演变(第四阶段)</h1>
  <h2>利用 IIFE 参数作为依赖声明使用</h2>
  <p>
    具体做法就是在第三阶段的基础上,利用立即执行函数的参数传递模块依赖项。
  </p>
  <p>
    这使得每一个模块之间的关系变得更加明显。
  </p>
  <script src="https://unpkg.com/jquery"></script>
  <script src="module-a.js"></script>
  <script src="module-b.js"></script>
  <script>
    moduleA.method1()
    moduleB.method1()
  </script>
</body>
</html>
// module-a.js

;(function ($) {
  var name = 'module-a'
  
  function method1 () {
    console.log(name + '#method1')
    $('body').animate({ margin: '200px' })
  }
  
  function method2 () {
    console.log(name + '#method2')
  }

  window.moduleA = {
    method1: method1,
    method2: method2
  }
})(jQuery)

// module-b.js

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

  window.moduleB = {
    method1: method1,
    method2: method2
  }
})()

总结:以上四个阶段是早期的开发者在没有工具和规范的情况下,对模块化的落地方式。这些方式却是解决了前端领域的模块化的问题,这只是仅仅解决了组织问题但模块化的加载问题。

  • Stage 5 -- AMD & CMD: AMD 和 CMD 是模块化规范初期落地实现的两个方案。
<!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 5</title>
</head>
<body>
  <h1>模块化规范的出现</h1>
  <h2>Require.js 提供了 AMD 模块化规范,以及一个自动化模块加载器</h2>
  <script src="lib/require.js" data-main="main"></script>
</body>
</html>
// main.js
require.config({
  paths: {
    // 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
    // 所以使用时必须通过 'jquery' 这个名称获取这个模块
    // 但是 jQuery.js 并不一定在同级目录下,所以需要指定路径
    jquery: './lib/jquery'
  }
})

require(['./modules/module1'], function (module1) {
  module1.start()
})
// ./modules/module1.js
// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 'jquery' 这个名称获取这个模块
// 但是 jQuery.js 并不在同级目录下,所以需要指定路径
define('module1', ['jquery', './module2'], function ($, module2) {
  return {
    start: function () {
      $('body').animate({ margin: '200px' })
      module2()
    }
  }
})
// ./modules/module2.js
// 兼容 CMD 规范(类似 CommonJS 规范)
define(function (require, exports, module) {
	// 通过 require 引入依赖
  var $ = require('jquery')
  // 通过 exports 或者 module.exports 对外暴露成员
  module.exports = function () {
    console.log('module 2~')
    $('body').append('<p>module2</p>')
  }
})

三、模块化规范的出现

在模块化规范出现之前,模块化的实现方式都是以原始的模块系统为基础,并通过约定的方式去实现模块化的代码组织。这些方式在不同的开发者去实施的时候会产生一些细微的差别,为了统一不同的开发者和不同项目之间的差异,就需要一个标准去规范模块化的实现方式。另外在模块化当中针对模块加载的问题,在前面四个阶段都是通过 script 标签手动的去引入每个用到的模块,这也就意味着模块加载并不受代码的控制一旦时间久了过后维护起来就非常的麻烦。试想一下如果某个页面中依赖了一个模块,而在 HTML中却忘记引用这个模块的话就会出现问题。又或者是在页面中移除了某个模块的引用,却忘记了在 HTML 当中去删除这个模块的引用这些都会产生很大的问题。所以说我们需要一些基础的公共代码去实现自动通过代码去帮我们加载模块,也就是说现在需要的是一个模块话的标准和一个自动加载模块的基础库(模块加载器)。

CommonJS 规范

提到了模块化规范,我个人首先想到的就是 CommonJS 规范,它是 node 中所提出的一套标准在 node 中所有的模块代码必须要遵循 CommonJS 规范。这个规范约定了每一个文件就是一个模块、而且每一个模块都有一个单独的作用域、每个模块需要通过 module.exports 导出成员、通过 require 函数载入模块。如果想在浏览器端也使用这个规范的话就会出现一些问题,因为 node 的模块加载机制是同步的方式。 node 的执行机制是在启动时去加载模块执行过程当中是不需要去加载的,它之后去使用到模块所以说这种方式在 node 当中不会有问题。但是如果换到浏览器端,在浏览器中使用 CommonJS 规范的话,必然导致效率低下因为每一次页面加载都会导致大量的同步模式请求出现。

AMD 规范

所以说在早期的前端模块化当中并没有选择 CommonJS 规范,而是专门为浏览器端结合浏览器的特点重新设计了一个规范 AMD(Asynchronous Module Definition) 异步模块定义规范,而且同期还推出了一个 AMD 规范的库 require.js 。它实现了 AMD 这个规范,另外它本身又是一个非常强大的模块加载器。在 AMD 这个规范当中约定每个模块都必须要通过 define 函数去定义,这个函数默认可以接收两个参数也可以传递三个参数。如果传递三个参数的话,第一个参数就是模块的名字在后期加载这个模块的时候使用、第二个参数是一个数组用来声明这个模块的一些依赖项、第三个参数是一个函数并且这个函数的参数与前面的依赖项一一对应每一项为模块导出的成员。define 函数的作用可以理解为当前模块提供一个私有空间,如果说想要向外部导出一些成员的话可以通过 return 的方式实现。

// 定义一个模块
define('module1', ['jquery', './module2'], function($, module2){
	return {
    	start: function() {
        	$('body').animate({margin: '200px'})
        }
    }
})

除此之外 requireJS 当中还提供了一个 require 函数用来帮助开发者自动加载模块,它的用法和 define 函数类似。区别是在于 require 函数只是用来去加载模块,而 define 函数是用来定义模块。

// 载入一个模块
require(['./module1'], function(module1) {
	module1.start()
})

一旦当 requireJS 需要加载一个模块的话,它内部会自动创建一个 script 标签去发送对应的脚本文件的请求并且执行相应的模块代码。 目前绝大多数的第三方库都支持 AMD 规范,但是 AMD 使用起来相对比较复杂,因为开发者在编写代码的时候不光要编写业务代表,并且还要使用 require、define 等这些操作模块的代码手动去编写模块文件。这些会导致我们的代码复杂程度有一个提高,另外如果模块当中如果模块划分的过于细致的话就会造成在同一个页面当中对 JS 文件请求次数就会特别多,从而导致页面效率就会比较低下。所以 AMD 是前端模块化演进中的一步,它是一种妥协的实现方式并不能算是最终的解决方案,只不过在当时的这个背景下面它还是非常有意义的。

CMD 规范

AMD 首次在浏览器端为前端模块化提供了一个标准,同期还出现了一个由淘宝所提出的 sea.js 库所实现的另一个标准 CMD(Common Module Defination)通用模块定义规范。这个模块标准有点类似于 CommonJS,在使用上和 requireJS 也基本上差不多可以说是一个重复的轮子。它当时的想法就是希望让 CMD 写出的代码尽可能的跟 CommonJS 类似从而减轻开发者的学习成本,但是这种方式在后来被 requireJS 也兼容了。

// CMD 规范
define(function(require, exports, module) {
	// 通过 require 引入依赖
    var $ = require('jquery')
    // 通过 exports 或者 module.exports 对外暴露成员
    module.exports = function () {
    	$('body').append('<p>module2</p>')
    }
})

四、模块化标准规范(模块化的最佳实践)

随着技术的发展 JS 的标准也在逐渐完善,在模块化方面的实现方式相对于以往已经有了一个很大的变化。现如今的前端模块化以经算是非常成熟了,而且目前针对前端模块化的最佳实践方式也都基本统一了。

  • node 环境遵循 CommonJS 规范
  • 浏览器环境遵循 ES Modules 规范 ES Modules 规范是 ES2015 定义的一个最新的模块系统,在最初刚推出的时候所有主流的浏览器基本上都是不支持这样一个特性的。但是随着 webpack 等一系列打包工具的流行,这一规范才逐渐开始被普及截止到目前 ES Modules 基本上可以说是最主流的前端模块化方案。相比于 AMD 这种社区提出来的开发规范,ES Modules 可以是说是在语言层面上实现了模块化,所以说它更为完善一些。另外现如今绝大多数的浏览器已经开始支持 ES Modules 这个特性了,原生支持也就意味着我们可以在以后直接去使用这样一个特性开发 web 应用。

五、ES Modules 基本特性

学习 ES Modules 可以从两个维度入手:

  1. 了解 ES Modules 作为一个规范或者说标准,约定了哪些特性和语法。
  2. 如何通过一些工具或者方案去解决 ES Modules 在运行环境当中兼容性所带来的问题。 接下来我们梳理下 ES Modules 的语法特性(通过给 script 标签添加 type = module 的属性,可以直接去使用 ES Modules 的标准去执行引入 JS 文件当中的 JS 代码):
<!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 - 模块的特性</title>
</head>
<body>
  <script type="module">
    console.log('this is es module')
  </script>
</body>
</html>
// demo.js
alert('hello')
  1. ESM 自动采用严格模式,忽略 'use strict'。严格模式的代表特征就是不能在全局范围使用 this,在非严格模式下 this 的指向是全局对象。
<script type="module">
  console.log(this)
</script>
  1. 每个 ES Module 都是运行在单独的私有作用域中。
<script type="module">
  var foo = 100
  console.log(foo)
</script>
<script type="module">
  console.log(foo)
</script>
  1. ESM 是通过 CORS 的方式请求外部 JS 模块的,这就意味着模块如果不在同源地址下面的话,就需要请求的服务端地址它在相应的相应头当中必须要提供有效的 CORS 目标头。另外 CORS 不允许通过文件的方式去访问,所以必须使用 HTTP Serve 的方式让页面工作起来。
<script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
  1. ESM 的 script 标签会自动的延迟执行脚本,这点等同于 script 标签的 defer 属性。也就是说添加 type = "module" 属性,就相当于添加了一个 defer 属性。网页的加载过程默认的对 script 标签是采用立即执行的机制,页面的渲染会等待脚本的执行完成才会继续去往下渲染。添加 type = "module" 属性相当于让脚本启用延迟执行的机制,延迟执行会等待网页的渲染完成过后再去执行脚本这个时候就不会阻碍页面当中元素的显示,这一点和添加 defer 属性是一样的。所以这个小特点也是 ES Modules 当中实际上对 script 标签做了一个小小的改进。
<!-- 等同于 defer --->
<script type="module" src="demo.js">
</script>
<p>需要显示的内容</p>

总结 ES Modules 包含下面四个特性:

  • 自动采用严格模式,忽略 ‘use strict’
  • 每个 ESM 模块都是单独的私有作用域
  • ESM 是通过 CORS 去请求外部的 JS 模块的
  • ESM 的 script 标签会延迟执行脚本

六、ES Modules 倒入和导出

导入和导出作为 ES Modules 的核心功能,主要是由 export 和 import 这两个关键词构成。export 命令是用于在模块内去对外暴露接口,而 import 命令则是在模块内去倒入其他模块所提供的接口。

<!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 导出与导入</title>
</head>
<body>
  <script type="module" src="app.js"></script>
</body>
</html>

ES modules 中每一个模块成员都会运行在独立的私有作用域当中,所以说模块内所有的成员都没有办法直接在外部被访问到。如果说我们需要去对外提供某些成员例如我们要把下面的 name 暴露出去,就必须要通过 export 关键词去修饰一下 name 的变量声明。

// module.js
export var name = 'foo module'

// app.js
import { name } from './module.js'
console.log(name) // foo module

除了可以使用 export 修饰变量的声明,还可以通过 export 去修饰函数的声明,这样的话函数也可以作为模块的导出成员。

// module.js
export function hello () {
	console.log('hello')
}
// app.js
import { hello } from './module.js'

hello() // hello

类的声明也是一样的,都是可以通过 export 这种方式去导出的。

// module.js
export class Person {
	constructor (name) {
    	this.name = name
    }
}
// app.js
import { Person } from './module.js'

const p = new Person('Leo')
console.log(p.name) // Leo

除了这种修饰成员声明这种使用方式,export 一般还可以用来单独去使用具体的操作方式就是,将变量声明之前的这些 export 都删除然后在模块的最底部导出对象的方式。

// module.js
var name = 'foo module'
function hello () {
	console.log('hello')
}
class Person {
	constructor (name) {
    	this.name = name
    }
}
export { name, hello, Person }
// app.js
import { name, hello, Person } from './module.js'
console.log(name) // foo module
hello() // hello
const p = new Person('Leo')
console.log(p.name) // Leo

除此之外还可以通过这种方式去为输出的成员重命名,具体的做法就是通过 as 关键词实现。

// module.js
var name = 'foo name'
export { name as fooName}
// app.js
import { fooName } from './module.js'
console.log(fooName) // foo name

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

// module.js
var name = 'foo name'
export {name as default}
// app.js
import {default as fooname} from './module.js'
console.log(foonname) // foo name

不仅如此在 ES Modules 当中还专门为 default 设计了一个特殊的用法,这个用法就是直接通过 export default 后面跟上一个变量,这样就可以将这个变量作为我们当前这个模块的默认导出,在导入的时候也可以通过直接 import 一个变量名的方式去接收这个模块默认导出的成员。这个变量名就可以根据需要随便去取。

export defualt var name = 'foo name'
// app.js 
import fooname from './module.js'
console.log(foonname) // foo name

七、ES Modules 导入导出的注意事项

针对 ES Modules 的导入和导出有一些需要注意个问题,这些小问题如果没有在意的话,很容易会因为这些小问题而出现一些错误。

  • 第一个问题就是我们在使用 export 导出一些成员的时候 export { name, hello } ,这种方式很多人会误认为 export 后面跟上的是一个对象的字面量,导入的时候是对这个对象的结构。实际上这种想法是完全错误的,export {}import {} 这是一种固定的语法。如果想导出一个对象的话可以使用 export default {} , 这个时候模块导出的就是一个对象;
  • 第二个问题就是在 ES Module 导出成员的时候,其实导出的是这个成员的引用。就是说 import 引入的是成员变量在内存中的引用地址,而不是对这个值的复制。
// module.js
var name = 'leo'
var age = 18
export {name, age}
setTimeout(function(){
	name = 'jone'
}, 1000)
// app.js
import {name, age} from './module.js'
console.log(name) // leo
setTimeout(function(){
	console.log(name) // jone
}, 1500)
  • 第三个问题是我们对外暴露一个成员的时候,其实暴露的是一个引用关系。而这个暴露出的引用关系它是一个只读的,也就是说我们不可以在模块外部修改这个变量。
// module.js
var name = 'leo'
var age = 18
export {name, age}
// app.js
import {name, age} from './module.js'
name = 'jone' // Uncaught TypeError: Assignment to constant variable

八、ES Modules 导入用法

对于 import 在使用上也有一些需要注意的点还有一些特殊的用法:

  • 第一点就是 import 在导入模块时 from 后面写的实际上是导入模块的路径,它是一个字符串在这个字符串当中必须要使用完整的文件名称,并且不能省略 .js 的扩展名,这里和在 CommonJS 里是有区别的。对于路径当中的 index.js 这部分在 CommonJS 当中也是可以省略掉的,通过载入目录的方式就是载入这个目录下 index, 但是通过实验发现在 ES Modules 原生的工作过程当中是不能载入 index 的,必须要手动的填写完整的路径。对于文件路径名称我们在后期去使用打包工具去打包我们的模块时,就可以省略扩展名也可以省略 index.js 这样一个默认文件的操作。如果导入的是一个相对路径,相对路径的 ./ 在 web 开发当中引用资源实际上是可以省略的。但是在 import 的时候 ./是不能省略掉的,如果省略掉的话就是以字母开头会造成 ES Modules 会认为加载第三方模块。所以说它必须要以 . 开头,这一点与 CommonJS 实际上是相同的。除了使用 ./ 这种相对路径,还可以使用 / 开头的决定路径也就是从网站根目录下开始往后去数。再者就是我们可以使用完整的 URL 去加载我们的模块。
  • 第二点就是如果说我们只是执行某个模块,而并不需要去提取这个模块当中的成员的话。我们就可以保持 import 后的 {} 为空 import {} from './module.js'。这样的话它就只会去执行这个模块,并不会去提取任何成员。这种方式还有一种简写的语法 import './module.js',这种特性在我们去导入一些不需要去外界控制的一些子功能模块是会非常有用。
  • 第三点就是如果说我们一个模块当中需要导出成员特别多,而且在导入时都会去用到它们这个时候就可以使用 import * from './module' 的方式,把这个模块当中所有导出的成员全部提取出来。提取出来过后需要通过 as 关键词将提取到的所有成员全部放到一个对象当中import * as mod from './module',这个导出成员都会作为这个对象的属性出现。
  • 第四点就是我们在使用导入模块的时候 import 关键词,可以理解成是一个导入模块的声明,它需要在开发阶段就明确我们需要导入的这个模块的路径。但是有的时候这个路径是我们在运行阶段我们才知道的,这种情况下就不能使用 import 关键词去 from 一个变量。有些时候我们需要在某些情况下当某些条件满足过后才去导入模块,在这种方式下也没有办法去使用 import 因为 import 关键词只能出现在最顶层,也就外侧的作用域并不能去嵌套在一些条件语句中。如果遇到上面两种情况就需要使用动态导入模块的机制了,ES Modules 中提供了一个全局的 import 函数专门用来动态导入模块。具体的用法就是通过 import 函数去传入我们需要去导入模块的路径 import('./module.js'),这个调用因为它是一个普通函数,在任何一个地方都可以去使用,而且这个函数返回的是一个 Promise 。因为模块的加载是一个异步过程,当模块加载完成过后会自动去执行 then 当中所指定的这样一个回调函数 import('./module').then(function(module){console.log(module)}),模块的对象可以通过参数去拿到。
  • 第五点如果我们在一个模块当中,同时导出了一些命名成员再导出了一个默认成员。
// module.js
var name = 'leo'
var age = 18
export default 'default'
export {name, age}
// app.js 方式 1
import {name, age, default as title} from './module.js'
console.log(name, age, title)
// app.js 方式 2
import title, {name, age} from './module.js'
console.log(name, age, title)

九、ES Modules 直接导出导入成员

除了导入模块 import 还可以配合 export 去使用,效果就是将导入的结果直接作为当前模块的导出成员。具体的用法就是我们把在 import 声明当中的 import 关键词修改为 export,这样当如的成员会直接做为模块的导出成员 export {foo, bar} from './module.js' 。这样在我们当前这个作用域当中,也就不再可以访问这些成员了。这样一个特性一般可以用在写 index 文件,通过 index 文件把某一个目录当中散落的一些模块,通过这种方式都给它组织到一起后导出,这样方便我们外部去使用。例如项目中常用 components 用来存放项目当中的组件模块

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

十、ES Modules 浏览器环境 Polyfill

了解了 ES Modules 当中的这些特性和语法过后,在回过头来去看下如何去解决运行环境的兼容性问题。正如前面所提到的 ES Modules 是 2014年才被提出来的,这也就意味着早期的浏览器不可能支持这个特性。另外在 IE 和一些国产的浏览器上截止到目前为止都还没有支持,所以说我们在使用 ES Modules 的时候还是需要去考虑兼容性的问题。Polyfill 可以帮助我们解决一些兼容问题,它可以让我们在浏览器当中直接去支持 ES Modules 当中绝大多数的特性。这个模块的名称叫做 ES Module loader,这个模块实际上就是一个 JS 文件,我们只需要将这个文件引入到网页当中就可以让这个网页去运行 ES Modules 了。

<!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>
export var foo = 'bar'

十一、ES