JavaScript 模块化方案 详细综述

1,665 阅读6分钟

1、概述

  1. 模块化要解决的问题:

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

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

    • 引用的 JS 模块会预先下载,但只有使用时才会执行;

    • 引用的 JS 模块直接下载,然后执行;

  2. 以前使用的模块化解决方案

    • CommonJS:用于服务器端(实现:Node.js);
    • AMD(Asynchronous Module Definition):用于浏览器端(实现:require.js、sea.js(sea.js又将其符合的规范化称为 CMD));
    • UMD(Universal Module Defination):整合CommonJS 和 AMD ,服务器端和浏览器端都可以使用(实现:require.js、sea.js);

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

  3. 现在的模块化解决方案

    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 模块的例子,可以克隆下来体验一下:gitee.com/hotpotliuyu…

这是一个sea.js 的一个 issue :github.com/seajs/seajs… 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

5、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 引擎根据入口文件加载模块。

  • import 命令也具有代码提升效果;

  • import 命令是静态执行的,它在编译阶段执行,因此多个 import 语句之间不能使用 表达式 和 变量这些动态语法,因此大多数时候要将 import 语句放在模块文件的顶部;

    const menu = '000';
    import b from 'a.js'; // 报错
    
  • 多次执行同一 import 语句,只会执行一次,import 导入模块使用的是单例模式;

    import {a} from 'modules.js';
    import {b} from 'modules.js';
    ​
    // 等同于
    import {a,b} from 'modules.js';
    
  • 目前可以通过 Balbel 转码,使 require 命令和 import 命令可以一起使用,但最好不要这样使用,因为import 是静态执行,require 是动态执行;



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 的语法





如果对你有帮助的话,点个 吧~ 如果想了解博主的更多内容:

GitHub:github.com/hotpotliuyu…

Gitee:gitee.com/hotpotliuyu…

公众号:火锅国技术部