# 16. Moudule模块

ES6之前,js没有module体系。社区制定了一些模块的加载方案,最主要有两种

  • 用于服务器:CommonJS (node里面 module.exports 与 require)
  • 用于浏览器:AMD (浏览器引入 require.js,define定义,require引入) http://javascript.ruanyifeng.com/tool/requirejs.html

ES6实现了模块功能,完全可以取代CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案

// CommonJS模块require
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

ES6 模块编译时就能确定模块的依赖关系,及输入输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。 ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

// ES6模块
import { stat, exists, readFile } from 'fs';

# export 命令

模块功能主要由两个命令构成:export和import。

  • export命令 用于规定模块对外接口
  • import命令 用于输入其他模块提供的功能 一个模块就是一个独立的文件,该文件内部所有变量,外部无法获取。如果希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。
export function multiply(x, y) {
    return x * y
}

// 可以使用as关键字
function v1() { ... }
function v2() { ... }
export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

注意不能直接 export 变量或函数,需要用 {}结构,或者直接在定义(初始化)时导出

export 1; // 报错

// 报错
var m = 1
export m

// 正确写法
export var m = 1
// 正确写法2
var m = 1
epxort {m}
// 正确写法3
var n = 1;
export {n as m}


// 导出函数
function f() {}
export f  // 报错

// 正确写法
export function f() {}
// 正确写法2
function f() {}
exporf {f}

注意事项

// 1. export输出的接口,与对应的值时动态绑定关系。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// 上面代码输出变量foo,值为bar,500 毫秒之后变成baz。

// 2. export 不能放到函数里面。
function foo() {
  export default 'bar' // SyntaxError
}
foo()

# import 命令

// main.js
import { firstName, lastName, year } from './profile.js';
function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

// improt里 as用法
import { lastName as surname } from './profile.js';

// 注意事项:
// 1. import的变量是只读的,不能直接赋值
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;  错误
a.foo = 'hello'; // 合法操作

// 2.improt 会提升到整个模块的头部,首先执行
foo();
import { foo } from 'my_module';

// 3.只执行一次
import 'lodash';
import 'lodash';

// 4.
import { foo } from 'my_module';
import { bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';

# 模块的整体加载

// circle.js
export function area(radius) {
  return Math.PI * radius * radius;
}
export function circumference(radius) {
  return 2 * Math.PI * radius;
}

// main.js
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));

// main.js 另一种导入方法
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

// 导入的circle不允许改变
import * as circle from './circle';
// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};

# export default 命令

// 1. 可以输出匿名函数 2. import时可以直接引入,不需要结构 {}
// export-default.js
export default function () {
  console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'


// 3. 输出非匿名函数也是可以的
// export-default.js
export default function foo() {
  console.log('foo');
}
// 或者写成
function foo() {
  console.log('foo');
}
export default foo;

// 4. 本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。
// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同于
// export default add;
// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';


// 5.
// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;

// 6.
// 正确
export default 42;
// 报错
export 42;

// 7. export default也可以用来输出类。
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();


// 8. 同时输入默认方法和其他接口
// loadsh.js 如下
export default function (obj) {
  // ···
}
export function each(obj, iterator, context) {
  // ···
}
export { each as forEach };

import _, { each, forEach } from 'lodash';

# export 与 import 的复合写法

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

// 2.
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';

// 3.
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;

# import() 函数

按需加载,运行到这一句,就会加载指定的模块。按需加载import() 返回一个Promise对象

const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main); 
  })  
  .catch(err => {
    main.textContent = err.message;  
  });

适用场景

// 1. 按需加载
button.addEventListener('click', event => {
  import('./dialogBox.js')
  .then(dialogBox => {
    dialogBox.open();
  })
  .catch(error => {
    /* Error handling */
  })
});

// 2. 条件加载
if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

// 3.动态的模块路径,根据f()返回结果,加载不能的模块
import(f()).then(...);

// 4.
Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),]).then(([module1, module2, module3]) => {
   ···
});

// 5.
async function main() {
  const myModule = await import('./myModule.js');
  const {export1, export2} = await import('./myModule.js');
  const [module1, module2, module3] =
    await Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ]);
}
main();

# Moudle的加载实现

# 浏览器加载

<!-- 1. 传统方法 --> 
<!-- 页面内嵌的脚本 -->
<script type="application/javascript">
  // module code
</script>

<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>

<!-- 异步加载 -->
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
<!-- defer是“渲染完再执行” -->
<!-- async是“下载完就执行” -->

ES6 type="module" 表示导入一个ES6 模块

<script type="module" src="./foo.js"></script>

<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

<!-- async 也可以 --> 
<script type="module" src="./foo.js" async></script>

<!-- ES6模块也允许内嵌在网页中 -->
<script type="module">
  import utils from "./utils.js";
  // other code
</script>

# ES6模块与CommonJS模块的差异

  • CommonJS 模块输出的是一个值得拷贝,ES6模块输出的是值得引用
  • CommonJS 模块是运行时加载,ES6模块是编译时输出接口
// CommonJS模块
// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

// main.js 
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3


// ES6 模块
// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

# ES6模块加载CommonJS模块

import 加载 module.exports 导出的变量、函数

// a.js
module.exports = {
  foo: 'hello',
  bar: 'world'
};
// 等同于
export default {
  foo: 'hello',
  bar: 'world'
};

// 写法一
import baz from './a';
// baz = {foo: 'hello', bar: 'world'};
// 写法二
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};
// 写法三
import * as baz from './a';
// baz = {
//   get default() {return module.exports;},
//   get foo() {return this.default.foo}.bind(baz),
//   get bar() {return this.default.bar}.bind(baz)
// }


// b.js
module.exports = null;
// es.js
import foo from './b';
// foo = null;
import * as bar from './b';
// bar = { default:null };

// 不能这样写,因为类似于export default,不能结构赋值
// 不正确
import { readFile } from 'fs';

# CommonJS模块加载ES6模块

CommonJS模块加载ES模块,不能使用require,而要使用import()函数

// es.mjs
let foo = { bar: 'my-default' };
export default foo;
// cjs.js
const es_namespace = await import('./es.mjs');
// es_namespace = {
//   get default() {
//     ...
//   }
// }
console.log(es_namespace.default);
// { bar:'my-default' }


// 例子2
// es.js
export let foo = { bar:'my-default' };
export { foo as bar };
export function f() {};
export class c {};
// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
//   get foo() {return foo;}
//   get bar() {return foo;}
//   get f() {return f;}
//   get c() {return c;}
// }

# 模块循环加载

a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本,待后续研究...

// a.js
var b = require('js')

// b.js
var a = require('a')

CommonJS模块无论加载对少次,只会运行一次,以后再加载,都是第一次运行的结果,除非清除缓存 require命令第一次加载该脚本,就会执行整个脚本,在内存中生成一个对象, 以后需要用到这个模块时,就会到exports属性里面取值,

上次更新: 2020/10/28 18:32:03