盘点前端模块化的那些事儿

290 阅读6分钟

一、模块化的概念

模块化是指自上而下把一个复杂问题(功能)划分成若干模块的过程,在编程中就是指通过某种规则对程序(代码)进行分割、组织、打包,每个模块完成一个特定的子功能,再把所有的模块按照某种规则进行组装,合并成一个整体,最终完成整个系统的所有功能

从基于 Node.js 的服务端 commonjs 模块化,到前端基于浏览器的 AMDCMD 模块化,再到 ECMAScript2015 开始原生内置的模块化, JavaScript 的模块化方案和系统日趋成熟。

TypeScript 也是支持模块化的,而且它的出现要比 ECMAScript模块系统标准化要早,所以在 TypeScript 中即有对 ECMAScript 模块系统的支持,也包含有一些自己的特点

二、模块化历程

  • CommonJS
  • AMD
  • UMD
  • ESM

无论是那种模块化规范,重点关注:保证模块独立性的同时又能很好的与其它模块进行交互

  • 如何定义一个模块与模块内部私有作用域
  • 通过何种方式导出模块内部数据
  • 通过何种方式导入其它外部模块数据

三、CommonJS:基于服务端、桌面端的模块化

在早期,对于运行在浏览器端的 JavaScript 代码,模块化的需求并不那么的强烈,反而是偏向 服务端、桌面端 的应用对模块化有迫切的需求(相对来说,服务端、桌面端程序的代码和需求要复杂一些)。CommonJS 规范就是一套偏向服务端的模块化规范,它为非浏览器端的模块化实现制定了一些的方案和标准,NodeJS 就采用了这个规范。

1.独立模块作用域

一个文件就是模块,拥有独立的作用域

2.导出模块内部数据

通过 module.exportsexports 对象导出模块内部数据

// a.js
let a = 1;
let b = 2;

module.exports = {
  x: a,
  y: b
}
// or
exports.x = a;
exports.y = b;

3.导入外部模块数据

通过 require 函数导入外部模块数据

// b.js
let a = require('./a');
a.x;
a.y;

四、AMD:基于浏览器的模块化

AMD:因为 CommonJS 规范一些特性(基于文件系统,同步加载),它并不适用于浏览器端,所以另外定义了适用于浏览器端的规范

AMD(Asynchronous Module Definition)

github.com/amdjs/amdjs…

浏览器并没有具体实现该规范的代码,我们可以通过一些第三方库来解决

1.requireJS

官网:requirejs.org/

(1)HTML

引入require.js文件,再通过data-main设置入口文件

// 1.html
<script data-main="js/a" src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>
(2)独立模块作用域

通过一个 define 方法来定义一个模块,在该方法内部模拟模块独立作用域。一个文件内可以有多个define方法,但建议一个文件只设置一个。

// b.js
define(function() {
  // 模块内部代码
})
(3)导出模块内部数据

通过 return 导出模块内部数据

// b.js
define(function() {
  let a = 1;
	let b = 2;
  return {
    x: a,
  	y: b
  }
})
(4)导入外部模块数据

通过前置依赖列表导入外部模块数据

// a.js
// 定义一个模块,并导入 ./b 模块
define(['./b'], function(b) {
	console.log(b);
})

2.requireJSCommonJS 风格

require.js 也支持 CommonJS 风格的语法

(1)导出模块内部数据
// b.js
define(function(require, exports, module) {
  let a = 1;
	let b = 2;
  module.exports = {
    x: a,
    y: b
  }
})
(2)导入外部模块数据
// a.js
define(function(require, exports, module) {
  let b = require('./b')
  console.log(b);
})

五、UMD:多端同构

严格来说,UMD 并不属于一套模块规范,它主要用来处理 CommonJSAMDCMD 的差异兼容,是模块代码能在前面不同的模块环境下都能正常运行。随着 Node.js 的流行,前端和后端都可以基于 JavaScript 来进行开发,这个时候或多或少的会出现前后端使用相同代码的可能,特别是一些不依赖宿主环境(浏览器、服务器)的偏低层的代码。我们能实现一套代码多端适用(同构),其中在不同的模块化标准下使用也是需要解决的问题,UMD 就是一种解决方式

// 封装逻辑
(function(root, factory){判断环境}(参数1:运行环境,参数2:工厂函数))
// 封装的 UMD

(function (root, factory) {
  	if (typeof module === "object" && typeof module.exports === "object") {
        // Node, CommonJS 环境下
        module.exports = factory();
    }
    else if (typeof define === "function" && define.amd) {
      	// AMD 模块环境下
        define(factory);
    } else {
      	// 不使用任何模块系统,直接挂载到全局
      	root.kkk = factory();  // kkk是自定义的,root是传进来的this,代表运行环境
    }
}(this, function () {
    let a = 1;
		let b = 2;

    // 模块导出数据
    return {
        x: a,
        y: b
    }
}));

六、ESM:模块化的大同世界

ECMAScript2015/ECMAScript6 开始,JavaScript 原生引入了模块概念,而且现在主流浏览器也都有了很好的支持,同时在 Node.js 也有了支持,所以未来基于 JavaScript 的程序无论是在前端浏览器还是在后端 Node.js 中,都会逐渐的被统一。

1.独立模块作用域

一个文件就是模块,拥有独立的作用域,且导出的模块都自动处于 严格模式 下,即:'use strict',必须先声明才能赋值。

script 标签需要声明 type="module"

2.导出模块内部数据

使用 export 语句导出模块内部数据

// 导出单个特性
export let name1, name2, …, nameN;
export let name1 = …, name2 = …, …, nameN;
export function FunctionName(){...}
export class ClassName {...}

// 导出列表
export { name1, name2, …, nameN };

// 重命名导出
export { variable1 as name1, variable2 as name2, …, nameN };

// 默认导出
export default expression;
export default function () { … }
export default function name1() { … }
export { name1 as default, … };

// 模块重定向导出
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;

3.导入外部模块数据

导入分为两种模式

  • 静态导入
  • 动态导入
(1)静态导入

使用 import 语句导入模块,这种方式称为:静态导入

静态导入方式不支持延迟加载,import 必须在模块的最开始

import defaultExport from "module-name";
import * as name from "module-name";
import { export } from "module-name";
import { export as alias } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
document.onclick = function () {

    // import 必须放置在当前模块最开始加载
    // import m from './m.js'

    // console.log(m);

}
(2)动态导入

此外,还有一个类似函数的动态 import(),它不需要依赖 type="module" 的 script 标签。

关键字 import 可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个 promise

import('./m.js')
  .then(m => {
    //...
});
// 也支持 await
let m = await import('./m.js');

通过 import() 方法导入返回的数据会被包装在一个对象中,即使是 default 也是如此