# 认识 ES Module

  • JavaScript 没有模块化一直是它的痛点,所以才会产生我们前面学习的社区规范:CommonJS、AMD、CMD 等,
    • 所以在 ES 推出自己的模块化系统时,大家也是兴奋异常。
  • ES Module 和 CommonJS 的模块化有一些不同之处:
    • 一方面它使用了 import 和 export 关键字;
    • 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式;
  • ES Module 模块采用 export 和 import 关键字来实现模块化:
    • export 负责将模块内的内容导出;
    • import 负责从其他模块导入内容;
  • 了解:采用 ES Module 将自动采用严格模式:use strict
    • 如果你不熟悉严格模式可以简单看一下 MDN 上的解析;
    • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode

# 案例代码结构组件

  • 这里我在浏览器中演示 ES6 的模块化开发:
<script src="./main.js" type="module"></script>
  • 如果直接在浏览器中运行代码,会报类似如下错误:

  • Access to script at file:// ...... from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, chrome-untrusted, https.

  • 这个在 MDN 上面有给出解释:

  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules

    • 你需要注意本地测试 — 如果你通过本地加载 Html 文件 (比如一个 file:// 路径的文件), 你将会遇到 CORS 错误,因为 Javascript 模块安全性需要。
    • 你需要通过一个服务器来测试。
  • 我这里使用的 VSCode,VSCode 中有一个插件:Live Server

# exports 关键字

  • export 关键字将一个模块中的变量、函数、类等导出;
  • 我们希望将其他中内容全部导出,它可以有如下的方式:
  • 方式一:在语句声明的前面直接加上 export 关键字
  • 方式二:将所有需要导出的标识符,放到 export 后面的 {} 中
    • 注意:这里的 {} 里面不是 ES6 的对象字面量的增强写法,{} 也不是表示一个对象的;
    • 所以: export {name: name},是错误的写法;
  • 方式三:导出时给标识符起一个别名

# export 声明语句

// 加上type="module"属性,告知加载的文件是个模块
<script src="./main.js" type="module"></script>
  • foo.js
export const name = "lain"
export const age = 16
  • main.js
import { name, age } from "./foo.js"
console.log(name) // lain
console.log(age) // 16
  • 注:不允许使用file协议加载模块化代码

# export 导出 和 声明分开

<script src="./main.js" type="module"></script>
  • foo.js
const name = "lain"
const age = 16
function foo() {
  console.log('foo function')
}
export { name, age, foo }
  • 注意: export后面这个大括号是固定语法,可不是对象
  • main.js
import { name, age, foo } from "./foo.js"
console.log(name) // lain
console.log(age) // 16
console.log(foo) // [Function: foo]

# 第二种导出时起别名

<script src="./main.js" type="module"></script>
  • foo.js
const name = "lain"
const age = 16
function foo() {
  console.log('foo function')
}
// 起别名
export {
  name as fName,
  age as fAge,
  foo as fFoo
}
  • 注意: export后面这个大括号是固定语法,可不是对象
  • main.js
import { fName, fAge, fFoo } from "./foo.js"
console.log(fName) // lain
console.log(fAge) // 16
console.log(fFoo) // [Function: foo]

# import 关键字

  • import 关键字负责从另外一个模块中导入内容
  • 导入内容的方式也有多种:
  • 方式一:import {标识符列表} from ' 模块 ';
    • 注意:这里的 {} 也不是一个对象,里面只是存放导入的标识符列表内容;
  • 方式二:导入时给标识符起别名
  • 方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上

# 普通导入

<script src="./main.js" type="module"></script>
  • foo.js
export const name = "lain"
export const age = 16
  • main.js
// 普通导入方式
import { name, age } from "./foo.js"
console.log(name) // lain
console.log(age) // 16

# 别名方式

<script src="./main.js" type="module"></script>
  • foo.js
export const name = "lain"
export const age = 16
  • main.js
import { name as fName, age as fAge, foo as fFoo } from "./foo.js"
console.log(fName) // lain
console.log(fAge) // 16
console.log(fFoo) // [Function: foo]

# 将导出的所有内容放到一个标识符中

<script src="./main.js" type="module"></script>
  • foo.js
const name = "lain"
const age = 16
function foo() {
  console.log('foo function')
}
export { name, age, foo }
  • main.js
import * as foo from "./foo.js"
console.log(foo) // Module {Symbol(Symbol.toStringTag): 'Module'}
console.log(foo.name) // lain
console.log(foo.age) // 16
console.log(foo.foo) // [Function: foo]

# 举个栗子 export 和 import 结合使用

  • 补充:export 和 import 可以结合使用
export { sum as fooSum } from './foo.js'
  • 为什么要这样做呢?

    • 在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中;
    • 这样方便指定统一的接口规范,也方便阅读;
    • 这个时候,我们就可以使用 export 和 import 结合使用;
  • 案例:这是一些工具函数,将它们全部导出实际运用

  • format.js

function timeFormat() {
  return "2021-11-25"
}
function priceFormat() {
  return "2021.11"
}
export {
  timeFormat,
  priceFormat
}
  • math.js
function add(num1, num2) {
  return num1 + num2
}
function sub(num1, num2) {
  return num1 - num2
}
export {
  add,
  sub
}
  • request.js
export function request() {
  
}
  • 我一般会建一个 index.js 文件,将它们全部导入进来,最后统一导出,我认为这种方式阅读体验会比较好

# 导出方式一

  • index.js
import { add, sub } from './math.js'
import { timeFormat, priceFormat } from './format.js'
export {
  add,
  sub,
  timeFormat,
  priceFormat
}
  • 这种方式比较繁琐,不方便,下面将使用简单一些的导出方式

# 导出方式二

  • index.js
export { add, sub } from './math.js'
export { timeFormat, priceFormat } from './format.js'
  • 这样直接导出比上面那种更加方便

# 导出方式三

export * from './math.js'
export * from './format.js'

# Default 用法

  • 前面我们学习的导出功能都是有名字的导出(named exports):

    • 在导出 export 时指定了名字;
    • 在导入 import 时需要知道具体的名字;
  • 还有一种导出叫做默认导出(default export)

    • 默认导出 export 时可以不需要指定名字;
    • 在导入时不需要使用 {},并且可以自己来指定名字;
    • 它也方便我们和现有的 CommonJS 等规范相互操作;
  • 注意:在一个模块中,只能有一个默认导出(default export);

  • foo.js

const foo = "foo value"

# 默认导出的方式一

export {
  // 默认导出
  foo as default
}
  • 等价于下面的这种写法

# 默认导出的方式二

  • 这种方式更加常见!
export default foo
  • 导入测试一下
import foo from './foo.js'
console.log(foo) // foo value
  • 注意:默认导出只能有一个

# import 用法

# import 函数

  • 通过 import 加载一个模块,是不可以在其放到逻辑代码中的,比如:

  • 为什么会出现这个情况呢?

    • 这是因为 ES Module 在被 JS 引擎解析时,就必须知道它的依赖关系;
    • 由于这个时候 js 代码没有任何的运行,所以无法在进行类似于 if 判断中根据代码的执行情况;
    • 甚至下面的这种写法也是错误的:因为我们必须到运行时能确定 path 的值;
  • 但是某些情况下,我们确确实实希望动态的来加载某一个模块:

    • 如果根据不懂的条件,动态来选择加载模块的路径;
    • 这个时候我们需要使用 import () 函数来动态加载;
  • foo.js

const foo = "foo value"
export { foo }
  • 可以把import是当成一个函数
  • import函数返回的结果是一个Promise
const promise = import("./foo.js").then(res => {
  console.log("res:", res.foo) // 2.res: foo value
})
console.log(promise) // 1.Promise { <pending> }

# import meta

  • import.meta 是一个给 JavaScript 模块暴露特定上下文的元数据属性的对象。

    • 它包含了这个模块的信息,比如说这个模块的 URL;
    • 在 ES11(ES2020)中新增的特性;
  • 所以也可以把import当成一个对象,作为对象时还有一个属性:meta

  • ES11新增的特性:meta属性本身也是一个对象: { url: "当前模块所在的路径" }

console.log(import.meta) // {url: 'http://127.0.0.1:5500/...'}

# ES Module 的解析流程

  • ES Module 是如何被浏览器解析并且让模块之间可以相互引用的呢?
    • https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
  • ES Module 的解析过程可以划分为三个阶段:
    • 阶段一:构建(Construction),根据地址查找 js 文件,并且下载,将其解析成模块记录(Module Record)
    • 阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
      • 阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中

# 不允许导入变量修改值

  • exporting module can update variable value...

  • ...but importing module cannot update variable value

  • foo.js

let name = "lain"
let age = 16
export { name, age }
  • main.js
import { name, age } from './foo.js'
console.log(name, age) // lain 16
// Uncaught TypeError: Assignment to constant variable.
setTimeout(() => {
  name = "saber"
  age = 17
}, 1000)
// 可以看出并没有修改成功 还是原来的值
setTimeout(() => console.log(name, age), 2000) // lain 16

# 相互引用

npm install webpack webpack-cli
  • foo.js
const name = "foo"
//commonjs 导出
module.exports = { name }
  • bar.js
const name = "bar"
const age = 100
//es module 导出
export { name }
  • index.js
//es module 导入
import { name } from './foo'
console.log(name) // foo
//commonjs 导入
const bar = require("./bar")
console.log(bar.name) // bar
  • npx webpack打包 -> dist文件
//main.js  npx webpack 打包后的文件
(()=>{var e={825:(e,o,r)=>{"use strict";r.r(o),r.d(o,{name:()=>t});const t="bar"},717:e=>{e.exports=
{name:"foo"}}},o={};function r(t){var n=o[t];if(void 0!==n)return n.exports;var a=o[t]={exports:{}};
return e[t](a,a.exports,r),a.exports}r.n=e=>{var o=e&&e.__esModule?()=>e.default:()=>e;return r.d(o,{a:o}
),o},r.d=(e,o)=>{for(var t in o)r.o(o,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:o[t]})}
,r.o=(e,o)=>Object.prototype.hasOwnProperty.call(e,o),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.
defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(() =>{"use strict";var e=r(717);console.log(e.name);const o=r(825);console.log(o.name)})()})();