1、概述

  1. 模块化要解决的问题:
  • 如何包装一个模块的的代码,使之不污染模块外的代码;
  • 如何唯一标识一个模块;
  • 如何在不增加全局变量的情况下将模块的 API 暴露出去;

浏览器端模块化要解决的问题:

  • 引用的 JS 模块会预先下载,但只有使用时才会执行;
  • 引用的 JS 模块直接下载,然后执行;
  1. 以前使用的模块化解决方案
  • CommonJS:用于服务器端(实现:Node.js);
  • AMD(Asynchronous Module Definition):用于浏览器端(实现:require.js、sea.js(sea.js又将其符合的规范化称为 CMD));
  • UMD(Universal Module Defination):整合CommonJS 和 AMD ,服务器端和浏览器端都可以使用(实现:require.js、sea.js);

但是它们都只能动态加载,不能实现静态编译时优化;

  1. 现在的模块化解决方案
    export、import
    ES6从语言层面提供了模块化功能,其思想是尽量的静态化,使得编译时就能确定模块的依赖关系;


2、用于服务器的 Node.js 的模块化解决方案(CommonJS)

Node.js 中使用 module.exports 来暴露模块,使用 require() 来引用模块;

1、代码示范

暴露模块:

// ./info.js
const students = {};
const teachers = {};
module.exports = {
    students,
    teachers
}

引用模块:

// ./index.js
const { teachers } = require('./info.js'); // 引用的整个模块会初始化为一个Module对象;

2、Node.js 模块化的原理

在执行模块代码之前,Node.js 将使用如下所示的函数封装器对其进行封装:

(function(exports, require, module, __filename, __dirname) {
// 模块代码实际存在于此处
});

3、模块内 module、exports、module.exports、require() 的含义;

  • module 是在模块内指向模块对象本身的一个引用;
  • module.exports 是 module 一个属性;属性值包含的是模块对外输出的制;
  • exports 是指向 module.exports 的一个变量;
  • require() 是用来加载模块的函数;返回的就是被加载模块的 module.exports;
    使用 require() 引用模块的原理是:
  • require 命令第一次加载脚本文件,会执行整个脚本文件,然后在内存中生成一个对象;
  • 以后再次使用到这个模块时,会从缓存中找到第一次生成的那个对象;

脚本代码在 require 的时候,会全部执行,这被称为“运行时加载

模块的循环加载问题:一旦某个模块被“循环加载”,就只输出已经执行的部分,还未执行的部分不会输出;


3、基于 sea.js 讲解浏览器端模块化解决方案

使用 require() 引用模块,使用 module.exports 暴露;并且所有模块都通过define 定义;

define(function(require,exports,module) {

    const { getAwards } = require('./sea-modules/serversApi');

	module.exports = { getAwards };
     
})

要注意的是如果要使用 sea.js,在 html 文件中,加入以下代码:

<!-- 引入 sea.js -->
<script src="./seajs-2.2.0/sea.js"></script>
<script>
    // 配置
    seajs.config({
        base:"./"
    })
    // 指定模块的入口文件,真实路径是配置中的 base 字段 + seajs.use() 内的 url,即 ./index
    seajs.use("index");
</script>

我写了一个使用 sea.js 模块的例子,可以克隆下来体验一下:https://gitee.com/hotpotliuyu/myStudyForToBeAFrontendDeveloper/tree/master/html/testSea

这是一个sea.js 的一个 issue :https://github.com/seajs/seajs/issues/1744,通过他的见解你可以更充分的了解 sea.js;



4、ES6 中的模块化解决方案

ES6 从语言层面提供了模块化解决方案,使用 export 来指定要输出的代码,使用 import 来输入代码;

eg:

暴露模块:

// ./info.js
const students = {};
const teachers = {};
export {
    students,
    teachers
}

引入模块:

// ./index.js
import { students as stus } from '.info.js'

使用 import 加载模块的原理是:

只加载 teachers,其它未使用的内容不加载,这被称为“编译时加载“;

编译时加载的好处:

  • 可以对模块进行静态分析(宏、类型检测);
  • 不需要 UMD 模块规范了;

ES6 的模块自动采用严格模式,顶层 this 指向 undefined;

1、export

export 用来规定暴露的接口;

const students = {};
const teachers = {};
export {
    students,
    teachers,
    teachers as teachers1
}
  • 可以使用 as 关键字来重命名对外暴露的接口,使得同一个接口可以被暴露多次;
  • 注意不能直接暴露变量对应的值,而要暴露变量;
export var a = 1; // 不报错

// 报错
var a = 1;
export a;

// 不报错
var a = 1;
export {
	a
}
  • export 输出的变量对应的是模块内部实时的值 ,这与 Commonjs 模块输出的是值的拷贝不同;
  • export 语句可以放在模块内最外层作用域的任何位置;

2、import

import 命令用来加载对应模块:

import { teachers as teachs } from './info.js';

import './info.js'; // 加载模块,但不加载任何内容到模块内
import * as info from './info.js'; // 将模块暴露的所有接口都加载都模块内;
  • import 后紧跟的变量名必须要和被导入模块暴露的接口变量名一致,但是可以使用 as 取别名;
  • import 输入的变量都是只读的
  • import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么会到./node_modules下去找,然后根据模块的 package.json文件的 mian字段,JavaScript 引擎根据入口文件加载模块。
  • 脚本路径文件的后缀名 .js不可以省略;这与原来的模块化规范使用 require() 引入模块文件时不写后缀名有区别;
  • import 命令也具有代码提升效果;
  • 多次执行同一 import 语句,只会执行一次;
  • 目前可以通过 Balbel 转码,使 require 命令和 import 命令可以一起使用;

3、export default

使用 export default 为模块指定默认输出;

export default 在一个模块内只能使用一次,既只能有一个默认输出,所以使用 import 导入时可以为输出任意命名,而不用和 export 命令一样导入和输出的变量名一定要一样;

// info.js
export default function(){
    // ...
}
// index.js
import getu from 'infof.js'

export default 命令本质上是使用了一个名为default的变量名;所以 export default 命令后不能跟变量声明语句,而只能跟一个值(与 export 的规定相反);

4、export 和 import 的复合写法,模块继承,跨模块常量的解决方案

export 和 import 的复合写法

export { teachers,students } from './info.js';

// 简单等同于
import { teachers,students } from './info.js';
export { teachers,students };

写成一行后,teachers 和 students 实际上并没有导入当前模块,只是相当于对外转发了这两个接口,导致当前模块并不能直接使用 teachers 和 students;

复合写法并不会转发 default 方法;

模块继承

通过 export 和 import 的复合写法,相当于继承了一个模块的输出;

// ./circle.js
export * from './info.js';
export var name = 'circle';
export default function() {
    // ...
}

./circle.js 模块继承了 ./info.js 的所有输出,并且有自己的输出;


参考资料

js模块化历程

JS模块化工具requirejs教程

sea.js文档

ES6 之 Module 的语法