模块化开发

327 阅读28分钟

模块化开发

模块化可以说是当前前端最重要的前端开发范式之一, 随着前端应用的日益复杂, 项目代码已经逐渐膨胀到了不得不花大量时间去管理的程度, 而模块化就是一种最主流的代码组织方式
=> 他通过把复杂的代码按照功能的不同划分为不同的模块, 单独维护的这种方式去提供开发效率, 降低维护成本, 而单就模块化这个词而言:
=> 他仅仅是一个是思想, 或者说是一个理论, 并不包含具体的实现, 所以说接下来就来一起学习一下:
如何在前端项目中去实践模块化这样一个思想以及目前行业中的一些主流的方式或者工具:

1.1、模块化演变过程

早起的技术标准根本没有预料到前端行业会有今天的规模发展, 所以说很多设计上的遗留问题就导致了现在去实现前端模块化会遇到很很多的问题, 虽然说现在这些问题都被一些标准或一些工具解决了, 但是, 他解决的演进过程, 是值得我们去思考的
=>接下来我们就一起了解在前端方向去落实模块化的演变过程

1.1.1、文件划分方式

最早期 JavaScript 中的模块化实际上就是基于文件划分的方式去实现的, 这也使 Web 中最原始的模块系统
=>具体的做法就是: 将每一个功能以及他相关的状态数据单独存放在不同的文件中, 约定每一个文件都是一个独立的模块
=>去使用模块, 就是将模块引入到页面中, 一个 script 标签就对应一个模块, 在代码中直接调用模块中的全局成员(这个成员有可能是一个变量, 也有可能是一个函数)

// module B: module B 相关状态数据和功能函数
var name = 'module-b';
function method1 () {
  console.log(name + '#method1');
}
function method2 () {
  console.log(name + '#method2');
}
//模块的使用
<body>
  
  <script src="module-b.js"></script>
  <script type="text/javascript">
  	method1();
    name = 'foo';
  </script>
</body>

=> 这规划总方式的缺点:

  • 污染全局作用于: 所有的模块都直接在全局范围中工作, 没有独立的私有空间, 这样就导致模块中所有成员都可以在模块外部被任意的访问或者修改;
  • 命名冲突问题: 模块一旦多了以后, 很容易产生命名上的冲突;
  • 无法管理模块依赖关系

总的来说: 这种方式完全依靠约定, 项目一旦上了体量以后, 就不行了;
=> 但是勒, 在这个过程中暴露了一些问题, 如果都能很好的解决, 就可以更好的实现模块化;

1.1.2、命名空间方式

在这个阶段中, 约定每一个模块只暴露一个全局对象, 所有的模块成员都挂载到这个对象下面, 具体的做法:
=>在 1.1.1 的阶段之上, 将每个模块包裹成一个全局对象的方式去实现, 有点像在模块内为模块内的一些成员添加匿名空间的感觉;

//module A: module A 相关状态数据和功能函数
var moduleA = {
  name: 'module-a',
  method1: function () {
  	console.log(this.name + '#method1');
	},
  method2: function () {
    console.log(this.name + '#method2');
  }
}
//模块的使用
<body>
  
  <script src="module-a.js"></script>
  <script type="text/javascript">
  	moduleA.method1();
    moduleA.name = 'foo';
  </script>
</body>

这种方式减少了命名冲突的问题, 但是任然没有私有空间, 模块内的成员任然可以在外部被修改; 另外模块之间的依赖关系也没有得到解决;

1.1.3、IIFE

在这种阶段中, 使用立即执行函数的方式为模块提供私有空间, 具体的做法:
=>将模块中每一个成员都放在一个函数提供的私用作用域中; 需要暴露给外部的成员, 可以通过挂载到全局对象上的方式去实现;

;(function () {
  var naem = 'module-b';
  function method1 () {
    console.log(name + '#method1');
  }
  function method2 () {
    console.log(name + '#methid2');
  }
  //对外暴露成员
  window.moduleB = {
		method1: method1,
    method2: method2
  };
})()
//模块的使用
<body>
  
  <script src="module-b.js"></script>
  <script type="text/javascript">
  	moduleB.method1();
    //模块私有成员无法访问
    console.log(methodB.name); // undefined
  </script>
</body>

这种方式实现了私有成员的概念, 私有成员只能在模块内部成员通过闭包的方式访问, 而在外部是没有办法使用的, 这样就确保了私用变量的安全;

还可以利用自执行函数的参数去作为依赖申明去使用, 这样就使得每一个模块直接的依赖关系变得更加明显;
=>例如下面的例子中依赖 jquery , 可以利用自执行函数去接受一个 jquery 的参数, 在立即调用时去传递 jquery 这样一个参数;

;(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)

这样, 在以后维护这个模块时就很清楚, 该模块依赖 jquery;

以上就是早期在没有工具和规范的情况下对模块化的落地方式 这些方式解决在前端领域去实现模块的各种各样的问题, 但是任然存在一些没有解决的问题;

1.2、模块化规范

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

1.2.1、CommonJs 规范

他是 nodeJs 中所提出的一套标准, 在 nodeJs 中所有的模块代码必须要遵循 CommonJs 规范; CommonJs 有如下规定:

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

如果在浏览器端也使用这个规范的话, 那就会出现一些问题: CommonJs 是以同步模式加载模块(因为 node 的执行机制是在 启动时去加载模块, 执行过程中是不需要去加载的, 只会使用到模块), 会导致页面的效率低下(每次页面加载都会导致大量的同步模式请求出现);
=>所以在早期的前端模块化当中并没有选择 CommonJs 规范, 而是专门为浏览器端结合浏览器的特点重新设计了一个规范: AMD

1.2.1、AMD 规范

AMD( Asynchronous Module Definition - 异步的模块规范) ,
=> 而且同期还推出了一个非常出名的库: Require.js, 他是想了 AMD 这个规范, 同时他也是一款强大的模块加载器;

在 AMD 中规定: 每一个模块都需要通过 define() 函数去定义

//定义一个模块
//define 函数: 默认接受两个参数, 也可以接受三个参数;
//传递三个参数时: 第一个参数为模块的名称(在后期加载该模块的时候使用); 第二个参数是一个数组, 用来申明该模块的依赖项;第三个参数是一个函数, 函数的参数和第二个参数一一对应,每一项分别为依赖项导出的成员, 该函数的作用是: 为该模块提供私有空间, 可以通过 return 的方式实现在模块中需要向外部导出一些成员
define('module1', ['jquery', './module2'], function ($, module2) {
  return {
    start: function () {
      $('body').animate({ margin: '200px' })
      module2()
    }
  }
})
//载入一个模块//用法和 define 函数类型, require 函只是用来加载模块, 一旦当 RequireJs 需要去加载一个模块时, 内部会自动去创建一个 script 标签去发送对应的脚本文件的请求, 并且执行相应的模块代码;require(['./module1'], function (module1) {  	module.start()})

目前绝大多数第三方库都支持 AMD 规范, 也就是说 AMD 的生态是比较好的, 但是 AMD 有以下的缺点:

  • 使用起来相对比较复杂

    因为在代码的编写过程中, 处理业务的代码, 还需要使用很多的 define 和 require 操作模块的代码, 这些导致代码的复杂程度有一定的提高;

  • 模块 JS 文件请求频繁

    当项目中模块划分过于细致, 在同一个页面中对 JS 文件的请求次数就会特别多, 从而导致页面的效率比较低下;

1.2.3、Sea.js + CMD

和 AMD 同期出现, 实现的是 CMD 标准: 通用的模块规范, 该模块的标准有点类似于 COmmonJs, 在使用上有点类似于 RequireJs, 但是的想法就是 让我们写出来的代码尽可能和 CommonJS 类似, 从而减轻开发者的学习成本;但是这种方式在后来被 RequireJs 兼容了;

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

P.S 这些历史对于在 和平时期 才接触前端的朋友尤为重要;

1.2.4、模块化标准规范

尽管以上的标准也都实现了模块化, 但是他们都或多或少存在一些让开发者难以接受的问题;
=> 随着技术的发展, JavaScript 的标准也在逐步完善, 而在模块化这块的实现方式相对于以往已经有了很大的变化, 现阶段的模块化已经算是非常成熟了
=> 目前, 针对于前端模块化的最佳实现方式也都基本统一了: **在 node 环境中, 遵循 CommonJs 规范去组织模块, 在浏览器环境中, 采用 ES Modules 的规范;**当然也有极少部门例外情况出现;

对于 ES Modules , 他是 ECMAScript 2015(ES6) 当中定义的最新的模块系统, 也就是说他是最近几年才制定的标准, 所以会出现兼容性问题; 在 ESModules 推出的时候, 所有主流浏览器都不支持这个标准; 随着 webpack 等一系列打包工具的流行, 这一规范才逐渐开始普及;

截止目前为止, ES Modules 可以说是最主流的前端模块化规范, 相比于 AMD规范, ES Modules 在语言层面上实现了模块化, 更加完善; 现如今绝大多数的浏览器已经支持 ES Modules

1.3、ES Modules

对于 ESModules 的学习可以从两个方面入手: 首先, 必须要了解他作为一个规范或者标准到底约定了哪些特性和语法; 其次, 如何通过一些工具或者方案解决在运行环境中兼容性所带来的的问题;

1.3.1、ES Modules 基本特性

1.3.1.1、通过给 script 添加 type = module 的属性, 就可以使用 ES Modules 的标准执行其中的 js 代码;
<body>    <!-- 通过给 script 添加 type = module 的属性, 就可以是使用 ES Modules 的标准执行其中的 js 代码 -->  <script type="module">  	console.log('this is es modules');  </script></body>

运行上面的页面, 在 console 的面板上打印出 this is es modules, 意味着 script 在 type 设置为 module 的情况下依然可以作为 js 正常执行, 只不过相对于普通的 script 标签会有一些新的特性;

1.3.1.2、在 ES Modules 中会自动采用严格模式, 忽略使用 ‘use strict’ 申明严格模式: 不添加 ‘use strict’ 也是严格模式
<body>    <script type="module">  	//严格模式的代表: 不能在全局模式下使用 this    console.log(this);  </script></body>

执行上面的代码: 打印出 undefined

<body>    <script type="text/javascript">  	//严格模式的代表: 不能在全局模式下使用 this    console.log(this);  </script></body>

执行上面的代码: 打印出 window

1.3.1.3、每个 ES Modules 都是运行在单独的私有作用域中
<body>    <script type="module">  	var foo = 100    console.log(foo)  </script>  <script type="module">  	console.log(foo)  </script></body>

运行上面的代码: 打印出 100 foo is not defined; 说明每个 ES Modules 都有一个私有作用域, 解决了变量的全局污染;

1.3.1.4、ES Module 是通过 CORS 的方式请求外部 Js 模块的

如果 Js 模块如果不在同源地址下面, 那就需要请求的服务端地址在响应的响应头当中必须需要提供有效的 CORS 标头;

<body>  <script type="module" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script></body>

上面的链接是不支持 CORS 的, 所以打印出 跨域的错误, 同时在 Network 中该请求也被终止;

<body>  <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script></body>

上面的链接是支持 CORS 的, 所以请求 jquery 是正常的;
=> CORS 不支持文件的形式去访问, 必须使用 http server 的 形式让页面工作起来;

1.3.1.5、ES Modules 的 script 标签会自动延迟执行脚本;

这点等同于 script 标签的 defer 属性, 通过添加 type=module 相当于添加 defer 属性;
=>网页的加载过程对 script 标签是采用立即执行的机制, 页面渲染会等待脚本执行完成才会继续往下渲染;

<body>  <script type="text/javascript">  	alert('hello')  </script>  <p>    需要显示的内容  </p></body>

运行上面的文件, 弹出对话框的时间页面需要显示的内容还没有呈现;=> 因为现在正在执行脚本; 点击确定, 脚本完成才能显示内容;

<body>  <script type="module">  	alert('hello')  </script>  <p>    需要显示的内容  </p></body>

运行上面的文件, 在弹出对话框的时候 需要显示的内容已经显示;

<body>  <script defer>  	alert('hello')  </script>  <p>    需要显示的内容  </p></body>

运行上面的代码, 弹窗的时候 需要显示的内容也已经显示;

1.3.2、ES Modules 导出

ES Modules 核心功能: 对模块的导入和导出;
=> 这两个功能主要是通过 export 和 import 构成, export : 命令是在模块内对外暴露接口, import 命令是在模块内导入其他模块所提供的接口;

项目文件夹下有三个文件: app.js index.html module.js
=>在 index.html 中以模块的形式载入了 app.js, 尝试在 app.js 中载入 module.js 所暴露的模块成员;

//index.html<body>  <script type="module" src="app.js"></script></body>

ES Modules 中每一个模块都会运行在独立的私有作用域中, 模块内的所有成员都不能直接在外部被访问, 需要对外提供某些成员, 需要用 export 关键词修饰申明: export var xxx;
=>export 不仅仅可以修饰变量, 还可以修饰函数: export function xxxx: 这个函数也可以做为模块的导出成员;
=>类的申明也是一样的: export class xxxx
=> ES Modules 除了这种每个修饰的方式, 一般是单独使用 export: 将单个成员的 export 去掉, 在模块尾部 export { xxx, xxx, xxx } => 这种用法更加常见一些, 因为一般在模块尾部集中导出模块的方式更加直观的描述了该模块向外提供了哪些成员, 更加容易理解代码;
=> 导出时, 可以通过 as 对导出成员重命名: export { xxx as xxx }
=> 重命名会有一个特殊的情况: 如果将导出成员的名称重命名为 default, 这个成员就会将最为模块默认导出的成员
=>在 ES Modules 中还为 default 提供了单独的用法: export default 变量 => 该变量就作为该模块的默认导出;

//module.js//通过单个申明成员导出/* export var name = 'foo modules'export function hello () {  console.log('hello')}export class Person {} *///在模块尾部导出var name = 'foo modules'function hello () {  console.log('hello ES Modules')}class Person {}//export { name, hello, Person }//通过 as 对导出成员重命名//export { name as fooName, hello, Person }//当导出成员被重命名为 default 时, 引入的时候必须要对 default 成员重命名(因为 default 是关键词);export { /* name as default, */ hello as fooHello }export default name

在外部通过 import 关键词载入模块暴露出来的成员;

//app.js//import { name } from './module.js'//当导出成员重命名后, 引入成员就需要用重命名后的名称//import { fooName } from './module.js'//当导出成员被重命名为 default 时, 引入的时候必须要对 default 成员重命名(因为 default 是关键词);//import { default as fooName } from './module.js'//默认导出可以直接通过一个变量名去接受import fooName from './module.js'console.log(fooName)

模块系统在工作的时候会自动请求 module.js 模块, 并将暴露出来的成员做为局部变量使用;

运行 index.html , 控制台打印出: foo modules

1.3.2.1、ES Modules 导入导出的注意事项
1.3.2.1.1、通过 ES Modules 导出: export { xxx, xxxx }

上面这种方式很多人会认为 export 后面跟的是对象的字面量, 这种任务是错误的, export 后跟的花括号({})是固定的语法, 不是对象的字面量
要导出对象, 需要使用 export default { xxx, xxxx } 的方式 =>这时 default 后面跟的花括号({})就是对象的字面量, 这样的用法和 export {} 完全不一样, export default 后面可以跟一个变量或者值, 而 export default {} 这个花括号会被 js 解析为一个对象;

1.3.2.1.2、通过 ES Modules 引入: import {xxx, xxxx}

ES Modules 通过 import {} 引入变量, 并不是解构赋值, 而是 ES Modules 的固定用法, 用来提取 模块导出的成员;

1.3.2.1.3、ES Modules 中导出成员时是导出成员的引用

以 var name = ‘foo’ 为例, 在模块中定义了 name 的变量, 可以理解为此时内存中有 name 的空间里面存储了 foo 这样一个值, 通过 export 将 name 导出, 在其他 js 中 name 被提起, 提起后的变量并不是复制了一份 name 给其他 js , 而是将 name 所在内存的空间编码给了其他 js, 所以在其他 js 中访问 name 始终都是访问到的模块中定义的 name 空间;

//module.jsvar name = 'foo modules'var age = 18setTimeout(function () { name = 'bar modules' }, 1000)export { name, age }
//app.jsimport { name, age } from './modules.js'console.log(name, age)setTimeout(function () {  console.log(name, age)}, 1500)

执行文件, 先打印出 foo modules 18, 1.5s 后打印出 bar modules 18

1.3.2.1.4、ES Modules 暴露出来的成员是只读的

ES Modules 对外暴露的成员时暴露的是引用关系, 而暴露出来的引用关系是只读的, 也就是说并不能在模块的外部去修改暴露出来的成员;

//module.jsvar name = 'jack'var age = 18export { name, age }
//app.jsimport { name, age } from './module.js'console.log(name, age)name = 'tom'setTimeout(function () {  console.log(name, age)}, 1000)

执行文件: 报错 => Assignment to constant variable(没有办法修改常量); 所以在提取出来的成员是一个常量;

可以根据这个特点去定义一些常量模块: 例如项目中的配置文件, 配置文件在外部只是读取而不能被修改;

1.3.3、ES Modules import 的用法

1.3.3.1、import 在导入模块时 from 后的路径

import 在导入模块时 from 后跟的导入模块的路径, 该路径是字符串类型, 这个字符串必须使用完整的文件名称, 不能省略 .js 的扩展名
=> 对于载入的模块为 index.js时, 必须填写完整的路径, 不能只写 index.js 的上级目录
=> 后期在使用打包工具打包模块时, 可以省略扩展名, 也可以省略 index.js 默认文件的操作;
=> 该路径如果使用相对路径, 相对路径中的 ./ 是不能省略的, 如果省略后, 该路径是以字母开头, ES Modules 会解析为加载第三方模块, 所以这里不能省略 ./
=> 该路径除了使用 ./相对路径外, 还可以使用 / 开头的绝对路径;
=>该路径还可以是使用完整的 url 去加载模块, 这种方式需要在 from 后跟上完整的 url 地址, 这也意味着可以直接引用 CDN 上的模块文件;

1.3.3.2、执行模块而不提取模块成员

需要执行某个模块而并不需要提起该模块里面的成员, 可以保持 import 后 {} 为空:

import {} from './module.js'

这种写法可以简写为

import './module.js'

这个特性在导如不需要外界控制的子功能模块时很有用;

1.3.3.3、模块导出成员很多

模块的导出成员很多, 而且在导入是都会用到导出的成员, 这时可以使用 * 的方式将所有的成员提取出来, 提取出来重命名放在一个对象中;

import * as mod from './modules.js

在使用时通过: mod.成员名称 使用该成员;

1.3.3.4、模块的动态导入

import 关键词可以理解为导入模块的申明, 需要在开发阶段就明确导入模块的路径, 但是有如下两分钟情况不能使用 import 导入模块:
1.模块的路径是在运行时才知道的, 这种时候不能使用 import from 一个变量;
2.还有导入模块必须满足某些条件后才能导入, 在这种情况也不能使用 import , 因为 import 关键词只能出现在最顶层, 并不能嵌套在 if 的判断中

这时就需要动态导入模块: 使用 import 函数动态导入模块

import('./module.js')

import() 函数在任何地方都可以调用, 而且该函数返回的是一个 Promise, 当模块加载完成后会自动加载 then 中指定的回调函数 => 模块的对象可以通过参数去拿到

import('./module.js').then(function (module) {  console.log(module)})
1.3.3.5、同时导出了命名成员和默认成员的引用

当一个模块同时导出了命名成员和默认成员, 导入成员是命名成员可以正常的使用 {} 导入, 默认成员使用 as 重命名导入;

import { name, age, default as title } from './module.js'

这里可以简写为:

import title, { name, age } from './module.js'

1.3.4、ES Module 导出导入成员

除了导入模块, import 还可以配合 export 使用, 效果就是将导入的结果直接作为当前模块的导出成员:
=> 模块头部: import { xxx, xxxx } fromt ‘./xxxx’ 修改为 export { xxx, xxxx } from ‘./xxxx’

/* import { foo, bar } from './module.js' */export { foo, bar } from './module.js'console.log(foo, bar);

执行文件: 报错 ReferenceError: foo is defined; 修改之后当前作用域中不能访问成员的;

这个特性在书写 index.js 的时候常被用到: 通过 index 文件将每个目录下散落的模块通过以上特性组织到一起导出, 方便外部使用;
1.3.4.1、示例

目录结构: app.js index.html module.js components/avatar.js components/button.js component/index.js

//index.html<body>  <script type="module" src="app.js"></script></body>
//app.jsimport { foo, bar } from './module.js'import { Avatar, Botton } from './component/index.js'
//module.jsvar foo = 'foo name', bar = 'bar name'export { foo, bar }
//avatar.jsexport var Avatar = 'Avatar Component'
//button.jsexport var Button = 'Buttom Component'
//index.jsexport { Avatar } from './avatar.js'export { Button} from './button.js'

如果在组件中导出的是默认成员, 提取成员时需要提取 default 并通过 as 重命名;

//button.jsvar Button = 'Button Component'export default Button
//index.jsexport { default as Button } from './button.js'export { Avatar } from './avatar.js'

1.3.5、ES Modules 浏览器环境 Polyfill (解决运行环境带来的兼容性问题)

ES Modules 是在 2014年被提出来的, 这也意味着早期的浏览器不可能支持这个特性; 截止目前: 在 IE 和一些国产的浏览器中都不支持该特性, 所以在使用 ES Modules 时需要考虑兼容性所带来的的问题;

示例目录结构: index.html module.js

//index.html<body>  <script type="module">  	import { foo } from  './module.js'    console.log(foo)  </script></body>
//module.jsexport var foo = 'foo name'

在 chrome 中输出 foo name; 在 IE 中没有任何的输出;因为 IE 不兼容 ES Modules;
=> 如果你对常用的编译工具了解, 可以借助一些常用的编译工具在开发阶段将 es6 代码编译为 ES5, 然后在执行;
=> 这儿先去介绍一个 polyfill, 让浏览器直接去支持 ES Modules: browser-es-module-loader.js
=> 这是一个 npm 模块: 针对于 npm 模块, 可以通过 unpkg.com 网站的 CDN 服务去拿到模块下的所有 js 文件; 找到 babel-brow-build.js 和 browser-es-module-loader.js 文件并引入到 html 中;
=> unpkg.com/browser-es-module-loader => unpkg.com/browser-es-module-loader@xxx/dist/ => 分别将 babel-browser-build.js 和 browser-es-module-loader.js 链接地址分别引入到页面;

//index.html<body>  <script src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>  <script 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>

第一个引入的并不是 browser-es-module-loader 的核心代码, 引入的是 babel 及时运行在浏览器环境的版本, 第二个引入的才是 browser-es-module-loader 的核心代码; 工作时, 通过 es-module-loader 将代码读出来, 交给 babel 去转换, 从而让代码能够正常工作;
=> 然后在 IE 中运行代码: 报错 => Promise 未定义, 这是因为在 IE 中不支持 es6 的特性, 所以还需要引入 Promise 的 polyfill
=> 通过 unpkg.com/promise-polyfill 找到链接并引入到页面

//index.html<body>  /*amend + */  <script src="https://unpkg.com/promise-polyfill@8.2.0/dist/polyfill.min.js"></script>  /*amend end */</body>

重新在 IE 中执行 index.html , 输出 foo name; 这时 es-module 在 IE 中已经可以正常工作了;
=> 工作原理: 将浏览器中不能兼容的语法交给 babel 去转换, 对于需要 import 进来的文件, 通过 ajax 去请求, 请求回来的代码再通过 babel 转换, 从而解决 IE 下的兼容性问题;
=> 现在还有个问题: 在支持 ES Module 的浏览器中, foo 模块会被执行两次, 原因是浏览器本身会执行一次, es-module-loader 也会执行一次;
=> 对于这个问题, 可以通过 script 的一个新属性来解决: nomodule, 如果 script 标签上添加了 nomodule 属性, 该 script 只会在不支持 ES Modules 的浏览器中执行;

//index.html<body>  /*amend c */  <script nomodule src="https://unpkg.com/promise-polyfill@8.2.0/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>  /*amend end */</body>

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

1.3.6、ES Module in Node.js 支持情况

ES Modules 作为 JavaScript 语言层面的模块化标准, 逐渐回去统一所用的 JavaScript 领域的所有模块化需求; nodeJs 作为 JavaScript 非常重要的应用领域, 目前已经开始逐步支持 ES Modules, 从 nodeJs 的 8.5 之后, 内部就以实验的特性支持 ES Modules, 也就是说: 在 nodeJs 中可以原生的使用 ES Modules 编写代码;

考虑到原来的 CommonJs 规范和 ES Modules 规范之间的差距还是比较大的, 所以目前 ES Modules 还是属于过渡的状态;
=> 接下来, 我们就在 node 中去使用 ES Modules, 注意: node 的版本必须高于 8.5

项目目录结构: index.js module.js

//index.jsimport { foo, bar } from './module.js'console.log(foo, bar)
//module.jsexport const foo = 'hello'export const bar = 'world'

要在 node 中直接使用 ES Modules , 需要将文件的扩展名修改为 .mjs: 修改完后目录结构为 => index.mjs module.mjs, 在修改的时候会提示是否需要修改 import 的路径 => 选择 否
=> 修改 index.msj

//index.mjsimport { foo, bar } from './module.mjs'console.log(foo, bar)

运行 index.js, 需要在启动的文件名前添加: --experimental-modules => node --experimental-modules index.mjs
=> 会打印 hello world => 这里会提示一个警告, 因为 ES Modules 还是生产特性, 希望不要在生产环境中使用;

1.3.6.1、ES Modules 对 Node 原生模块的支持

通过 ES Modules 导入 node 原生的模块

//index.mjs/*amend + */import fs from 'fs'fs.writeFileSync('./foo.txt', 'foo Module')

运用 index.mjs, 在根目录下新增了 foo.txt, foo.txt 里面为 foo Module, 这就意味着在 Node 中可以通过 ES Modules 载入原生的模块;

1.3.6.2、ES Modules 对 Node 原生模块的成员提取

编辑 index.mjs

/*amend + */import { writeFileSync } from 'fs'writeFileSync('bar.txt', 'bar Module')

运行 index.mjs: 根目录下新增 bar.txt, bar.txt 里面为 bar module, 说明 ES Modules 可以提取 Node 原生模块里面的成员, 原因是: Node 原生模块, Node 都做了兼容, Node 会对原生模块的成员单独导出, 也会再导出一个对象;

1.3.6.3、ES Modules 对 Node 第三方模块的支持

这里以 lodash 为例, 先安装 lodash: cnpm i lodash -D, 编写 index.mjs

//index.mjs/*amend + */import _ from 'lodash'console.log(_.camelCase('ES Module'))

执行 index.mjs , 会打印出: esModules, 说明 ES Modules 可以载入第三方模块;

1.3.6.4、ES Modules 对 Node 第三方模块的成员提取

编辑 index.mjs

//index.mjs/*amend + */import { camelCase } from 'lodash'console.log(camelCase('ES Module Working'))

执行 index.mjs, 报错: The requested module ‘lodash’ does not provide an export named ‘camelCase’(lodash 模块并没有向外暴露 camelCase 的成员), 原因 import 后的 {}, 不是结构语句, 而第三方模块都是导出对象, 这个对象作为模块的默认成员导出;

不能以这种方式提取第三方模块成员, 这时必须使用默认导入的方式提取成员;

1.3.7、ES Modules in Node.js 与 CommonJS 模块交互

这里学习如何在 ES Modules 中载入 CommonJs 模块;
=> 目录结构: commonJs.js esModule.mjs

1.3.7.1、ES Modules 中可以导入 CommonJs 模块
//commonJs.jsmodule.exports = {  foo: 'commonJs exports value'}
//esModules.mjsimport mod from './commonJs.js'console.log(mod)

执行 esModules.mjs => 输出: { foo: ‘commonJs exports value’ } => 意味着 ES Modules 中可以导入 CommonJs 模块

在 CommonJs 中可以使用 exports 别名导出 => exports 就是 module.exports 的别名;

//commonJs.js// module.exports = {	//foo: 'commonJs exports value'//}exports.foo = 'commonJs exports value'

执行 esModules.mjs => 输出: commonJs exports value

1.3.7.2、CommonJs 始终只能导出一个默认成员

CommonJs 模块始终只能导出一个默认成员, 在 ES Modules 中提取成员时只能以 提取默认成员的方式;

//esModules.mjs// import mod from './commonJs.js'//console.log(mod)import { foo } from './commonJs.js'console.log(foo)

执行 esModules.mjs => 输出: SyntaxError: The requested module ‘./commonJs.js’ does not provide an export named ‘foo’(commonJs.js 没有导出 foo 成员)
=> 在 commonJs.js 中使用 exports.foo 导出成员, 在 ES Modules 中可以提取成员
=> 注意 import 不是解构导出对象

1.3.7.3、CommonJs 中不能导入 ES Modules 模块
//esModules.mjs => 注释之前的代码export const bar = 'es modules export value'
//commonJs.js => 注释之前的代码const mod = require('./esModule.mjs')console.log(mod)

执行 commonJs.js => 输出: Error: Must use import to load ES Modules => 说明 不能在 CommonJs 模块中通过 require 载入 ES Modules 模块
=> 在 webpack 打包, 会支持载入的, 但是在 Node 原生中是不允许的;

1.3.8、ES Modules in Node.js 与 CommonJs 模块的差异

项目目录结构: cjs.js esm.mjs

//cjs.js//记载模块函数console.log(require)//模块对象console.log(module)//导出对象别名console.log(exports)//当前文件的绝对路径console.log(__filename)//当前文件所在目录console.log(__dirname)

按照 CommonJs 的标准打印了5个成员, 这5个成员是 CommonJs 中模块的全局成员, 可以理解为全局变量; 实际上, 这5个成员是模块内置的;

执行 cjs: node cjs.js => 都能正常打印;

//esm.mjs//分别打印 cjs.js 中的5个成员

通过 nodemon 运行 esm.mjs: nodemon --experimental-modules esm.mjs => 这5个成员在 ES Modules 中全部无法访问; 意味着在 ES Modules 中不能使用这5个成员, 原因是: 这5个成员都是 CommonJs 将模块包装为一个函数之后, 通过参数提供进来的成员, ES Modules 的加载方式发生了变化, 所以都不提供这几个成员了;
=> 这5个成员中 module、require、exports 可以使用 ES Modules 中的 import 和 export 代替; 对于 --filename 和 __dirname 可以 import.meta.url 来代替;

//esm.mjsconsole.log(import.meta.url)

这个 url 是当前工作文件的文件 url 地址,
=> 可以借助于 url 模块中的 fileURLToPath 将文件 URL 转化为路径;

//esm.mjsimport { fileURLToPath } from 'url'const __filename = fileURLToPath(import.mate.url)console.log(__filename)

得到的 __filename 和 CommonJs 中 路径一样;

对于 dirname 可以借助于 path 模块中的 dirname() 方法: 该方法提取路径的文件夹部分;

//esm.mjs/*ament + */import { dirname } from 'path'const __dirname = dirname(__filename)console.log(__dirname)

得到的 __dirname 和 CommonJs 中路径一样

1.3.9、ES Modules in Node.js 新版本近一步支持 ESM

在 node 的最新版本中近一步支持了 ES Modules

项目目录结构: index.mjs module.mjs

//module.mjsexport const foo = 'hello'export const bar = 'world'
//index.mjsimport { foo, bar } from './module.mjs'console.log(foo, bar)

执行 index.mjs => 输出: hello world
=> 在新版本中, 可以给项目的 package.json 中添加 type: module 字段, 这时项目下的所有 JS 文件默认以 ES Modules 工作, 不用将 js 的扩展名修改为 mjs了(将 index.mjs 还原为 index.js);

现在在项目中使用 CommonJs 规范, 语法会报错, 如果还需使用 CommonJs, 需要将 CommonJs 的 js 文件的扩展名修改为 cjs

//common.jsconst path = require('path')console.log(path.join(__dirname, 'foo'))//现在去执行 common.js 会报错;//修改 common.js 的扩展名为 cjs => common.cjs, 重新执行 => 正常执行

1.3.10、ES Modules in Node.js Babel 兼容方案

使用早期 node 的版本, 可以使用 babel 实现 ES Modules 的兼容;
=> babel 是目前最主流的 JS 编译器, 可以用来将使用了新特性的代码编译为当前环境兼容的代码, 有了 babel 后就可以在绝大多数环境中使用新特性了;
=> 这里介绍在低版本 node 下使用 babel 兼容 ES Modules

项目目录结构: index.js module.js

安装 babel

cnpm i @babel/node @babel/core @babel/preset-env -D

完成之后就能找到 babel-node 的命令, 此时都可以在命令行中运行这个命令 => 运用 babel-node 命令

yarn babel-node
//module.jsexport const foo = 'hello'export const bar = 'world'
//index.jsimport { foo, bar } from './module.js'console.log(foo, bar)

通过 babel-node 运行 index.js: yarn babel-node index.js
=> 输出: 报错 SyntaxError: Unexpecte token import(import 不被支持)

原因: 因为 babel 是基于插件机制去实现的, 核心模块并不会转换代码, 要转化代码的每个特性是通过插件实现的, 现在需要一个插件去转化代码的每个特性;=> perset-env => 插件集合, 在集合中包含了最新的 js 标准的所有新特性, 这里使用 perset-env 将代码中的 ES Modules 转换;
=> 用法: 在使用 babel-node 执行代码时添加上 --presets=@babel/preset-env 的参数 => 这时都可以正常工作了;
=> 每次这样传入参数很麻烦 => 将该参数放在配置文件中 => 在项目根目录下添加 .babelrc 文件, 编辑 .babelrc

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

此时执行代码就需要添加 presets 参数了: yarn babel-node index.js

实际上转换代码的是一个插件, 并不是 preset-env, 这里可以使用单独的插件也可以实现转化
=> 这里使用插件: plugin-transform-modules-commonjs

安装该插件:

cnpm i @babel/plugin-transform-modules-commonjs -D

修改 .babelrc

{  "plugins": [ "@babel/plugin-transform-modules-commonjs" ]}

通过 babel-node 执行 index.js 正常执行;