前端模块化发展

94 阅读3分钟

发展流程

graph LR
a("无模块化时期") --> b("IIFE(立即执行函数表达式)")
a -.-> a1("<script />引入")
b --> c(CommonJS)
c --> d("AMD")
d --> e("CMD")
e --> f("ESModule")
f --> g("前端工程化")
a1 -.-> a2["缺陷:变量全局污染"]
c -.-> c1("require/exports.module")
c1 -.-> c2["缺陷:无法异步"]
d -.-> d1("使用require.js")
d1 -.-> d2["缺陷:无法按需加载"]
f -.-> f1("import/exports")
名称基本内容是否允许异步引入缺点加载模式
无模块化时期script标签引入不同js同步全局污染
IIFE立即执行函数同步不运行异步引入
CMJ(CommonJS)exports.module和require可异步运行时加载
AMD可异步运行时加载
CMD可异步运行时加载
ESM(ESModule )export及import可异步编译时加载

该异步指的是在js过程中再引入,同步值必须先加载完再执行

1、无模块化时期

无模块时,直接将所有的js逻辑放到一个js文件

<script src="./main.js"></script>

初步模块化理念:不同的js文件做各自对应的事情

<script src="./a.js"></script>
<script defer src="./b.js"></script>
<script async src="./c.js"></script>

引发问题:全局的污染

script标签存在defer属性和async属性

1414709-20180822191511672-1951871802.png 红色代表js脚本执行时间,绿色代表html解析。

defer和async都是异步加载,同步解析,只是async解析会打断当前html,defer是等当前html执行完再执行。

2、IIFE(立即执行函数表达式)

立即执行函数表达式创建了一个私有的作用域,在这个作用域内他们享有自己的变量和方法,不会影响到他的外部作用域。

(() => {
  let count = 1;
  const increase = () => count++;
  const getCount = () => count
})()

早期jqury这些框架也是利用这种revealing的写法去实现的,jqury其实就是IIFE挺好的一个实践

const currentModule = ((dependencyModule) => {
  // dependencyModule 引用依赖的模块
  let count = 1;
  const increase = () => count++;
  const getCount = () => count

  return { increase, getCount }
})(dependencyModule)

currentModule.increase();
currentModule.getCount();

3、CommonJS(CMJ)

CommonJS是node.js为了解决模块化问题提出的解决方案,这是一个同步的模块化解决方案,常用于服务器运行时加载

// module.js
const count = 1;
const increase = () => count++;
const getCount = () => count;

node对每个module内置了exports以及module两个变量
一般来说exports = module.exports
但是如果将exports指向一个固定的值而不是通过引用的方式,则会切断exports和module.exports的关系

exports.increase = increase;
exports.getCount = getCount;
or
exports.module = { increase, getCount }

// moduleB.js
const module = require(/module);
module.increase();
module.getCount();

fs加载实际上是生成一个fs对象,再从该对象上读取相应方法。所以只有在运行时才能得到对象。 CommonJS模块输出的是一个值的拷贝

新问题:怎么去解决异步依赖的问题

4、AMD

通过异步加载,允许执行回调函数,常用于浏览器运行时加载

define('currentModule', [dependency1, dependency2], (dependency1, dependency2) => {
  // do something;
return { increase, getCount }
})

// moduleB
require(['currentModule'], (module) => {
  module.increase();
})

问题:如果本身我们运用同步的方案那怎么在我们同步的方案中兼容AMDAMD内部提供了这样的方式
define('currentModule', [], require => {
  const dependency1 = require('dependency1')
  const dependency2 = require('dependency2')
  return {}
})

5、CMD

CMD解决按需加载的问题

define('currentModule',[],(require, exports, module) => {
  const dependency1 = require(dependency1)
  // do something
  const dependency2 = require(dependency2)
  // do something
})

6、ESModule

特性:能够兼容浏览器端和服务器端,并且统一了使用的语法。ESModule是编译时加载

// module
const count = 1;
const increase = () => count++;
const getCount = () => count;

exports default { increase, getCount }


// moduleB
import module from 'module';

module.increase();


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

除此之外。ESModule还提供了动态加载模块的解决方案,结合了promise的思想
import('module').then((module) => { xxx })

ESModule输出的是值的引用。遇到import不会取执行模块,只是生成一个动态引用,等到真正用的时候再从模块里面取值。 不同的脚本加载一个相同的模块,得到的都是同样的实例。

参考

前端模块化发展历程