五分钟带你回顾前端模块化发展史

2,306 阅读6分钟

观感度:🌟🌟🌟🌟🌟

口味:麻酱面藕

烹饪时间:10min

CSS 早在 2.1 的版本就提出了 @import 来实现模块化,但是 JavaScript 直到 ES6 才出现官方的模块化方案 ES Module。尽管早期 JavaScript 语言规范上不支持模块化,但这并没有阻止 JavaScript 的发展。官方没有模块化标准,那么我们就自己动手创建标准。社区里的前辈们创建并实现了规范,这些规范便是前端模块化发展之路上智慧的结晶。

模块化的价值

2020 年的今天来看,模块化应该具有以下价值:

  • 可维护性
  • 减少全局污染
  • 可复用性
  • 方便管理依赖关系
  • 分治思想的实践

十年之前,模块化还只是使用闭包简单的实现一个命名空间。使用这种解决方式可以简单粗暴的处理全局变量和依赖关系等问题。

关于闭包的概念可以参考我的另一篇文章

CommonJS

让我们把时间追溯到 2009 年 1 月,在这个普通的冬天里,万物开始生长。中国南极科学考察站昆仑站落成,成为南极海拔最高的科学考察站。在 JavaScript 社区中, Mozilla 的工程师 Kevin Dangoor 发起了 CommonJS 的提案。(最初叫ServerJS)

再到后来横空出世的 Node.js 采用 CommonJS 模块化规范,同时还带来了 npm (全球最大的模块仓库) 。

nodejs 中模块的实现并非完全按照 CommonJS 的规范来的,而是进行了取舍,并增加了少许特性。

CommonJS 规范在服务端表现出色,使得 JavaScript 在服务器端大放异彩,与传统服务器语言(PHP、Python)等产生抗衡甚至压倒之势。程序员们便萌发出了将它移植到浏览器端的想法。

然而由于CommonJS的模块加载是同步的。我们知道,服务器端加载的模块从内存或磁盘中加载,耗时基本可忽略。但是在浏览器端却会造成阻塞,白屏时间过长,用户体验不够友好。

因此,从CommonJS中逐渐产生了一些分支,也就是业内熟知的AMDCMD等。

AMD规范

James Burke提出了 AMD 规范,RequireJS 也是他的代表作。他同时开发了 amdefine(在node中可以使用AMD规范的库)。

AMD 主要是为了解决 CommonJS 规范在浏览器端的不足:

  • 缺少模块封装的能力
  • 使用同步的方式加载依赖
  • export 只能导出变量,导出函数需要用 module.export (这通常不符合直觉)

AMD 规范定义了一个 define 全局方法用来定义和加载模块,RequireJS 后期也扩展了 require 全局方法用来加载模块 。其核心实现是内部的模块加载器。

CMD规范

@玉伯提出了 sea.js (CMD规范的实现)。

准确的说 CMDSeaJS 在推广过程中对模块定义的规范化产物。

相比于AMD的异步加载,CMD更倾向于懒加载,规范本身也与CommonJS更贴近。

因为是懒加载机制,所以 sea.js 提供了 seajs.use 方法,来运行已经定义的模块。所有 define 的回调函数都不会立即执行,而是将所有的回调函数进行缓存,只有 use 之后,以及被 require的模块回调才会执行。

RequireJS 和 sea.js 的区别

  • sea.js 只有在 require 的地方,才会真正执行模块。
  • RequireJS 会先运行所有的依赖,得到所有的结果后再执行回调。

AMD 和 CMD 区别

  • 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。
  • CMD 推崇依赖就近,AMD 推崇依赖前置。
  • AMDAPI 一个当多个用,职责单一。CMD 中,每个API都简单纯粹。

来自@玉伯的回答

UMD

UMDAMDCommonJS 的综合产物。AMD 用于浏览器,CommonJS 用于服务器。UMD 则是两者的兼容模式,解决了跨平台问题。

实现原理:if-else

详情请移步githubUMD

ES Module

天下大势,分久必合。

十年之后,官方爸爸推出的 ES6 模块化方案,一统浏览器和服务器。采用了完全静态化的方式进行模块加载。

语法

模块导入

// 默认导入default模块
// main.js
import name from './module.js'

// module.js const name = '前端食堂' export default name

// 如果想导入其他模块
// main.js
import { name, getName } from './module.js'

// module.js
export const name = '前端食堂'
export const getName = () => name
// 同时导入
// main.js
import  name, { getName } from './module.js'

// module.js
const name = '前端食堂'
export const getName = () => name
export default name
// 重命名
// main.js
import * as mod from './module.js'
let name = ''
name = mod.name
name = mod.getName()

// module.js
export const name = '前端食堂'
export const getName = () => name
// 对单独的变量进行重命名
import { name, getName as getModName }

模块导出

// 第一种写法
export const name = '前端食堂'

// 第二种写法 export function getName() { return name } export class Logger { log(...args) { console.log(...args) } }

// 第三种写法 const name = '前端食堂' function getName() { return name } class Logger { log(...args) { console.log(...args) } } export {name, getName, Logger}

// 第四种写法 const name = '前端食堂' export default name

这里只提供了一些基本语法,更多语法请参考阮一峰老师的 《ECMAScript 6 入门》

当然,不同的规范之中,被规范的语法也有所不同,这里推荐业内公认的语法规范airbnb

这里列举出几个airbnb中模块导出/引入的规范。

1.如果模块只有一个输出值,就使用 export default ,如果模块有多个输出值,就不使用 export defaultexport default 与普通的 export 不要同时使用。

2.模块导入时不要使用通配符。因为这样可以确保你的模块之中,有一个默认输出 (export default)

// bad
import * as myObject from './importModule';

// good import myObject from './importModule';

ES Module与CommonJS的差异

1.语法 import/export require/module

2.CommonJS 模块输出的是一个值的拷贝(不会随原始值变化),ES6 模块输出的是值的引用(会随着原始值变化)。

3.CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

写在最后

成天讨论这门语言好,或者那门语言坏的人,甚至是可悲的。既悲其一叶障目,更悲其大愚若智的自得心态。 ——《大道至简》

JavaScrit 至今仍有被人诟病的缺憾之处。但历史告诉我们,再多的阻力也阻挡不了真正热爱它的人们。

感谢为 JavaScript 模块化之路贡献的开发者们。

❤️爱心三连击

1.看到这里了就点个支持下吧,你的赞是我创作的动力。

2.关注公众号前端食堂,你的前端食堂,记得按时吃饭!

3.特殊阶段,带好口罩,做好个人防护。